`
cues
  • 浏览: 3616 次
  • 性别: Icon_minigender_1
  • 来自: 长沙
最近访客 更多访客>>
社区版块
存档分类
最新评论

JavaScript变量及其作用域

阅读更多

本文将继续就如下几点做出一些归纳,更多的是对ECMA-262中相应知识点的一些翻译并掺杂笔者的一些拙见,仅供参考。由于对个别专业词汇的解释拿捏不定,在罗列demo的同时,对于结论的后续推断将留给读者自身分析一些余地,见谅。

 

在X贴过程中,笔者参阅了ECMA-262(3rd,5th,以下简称文档),Lich_Ray的中文翻译项目组提供的文档(http://code.google.com/p/ecma-262/),一并表示感谢:)

 

  • JavaScript变量基本常识
  • 浅析JavaScript变量作用域

JavaScript变量基本常识

1、js变量定义语法(全局变量、局部变量)

ECMA-262 写道
If the variable statement occurs inside a FunctionDeclaration, the variables are defined with function-local
scope in that function, as described in s10.1.3. Otherwise, they are defined with global scope (that is, they
are created as members of the global object, as described in 10.1.3) using property attributes { DontDelete
}. Variables are created when the execution scope is entered. A Block does not define a new execution
scope. Only Program and FunctionDeclaration produce a new scope. Variables are initialised to undefined
when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the
VariableStatement is executed, not when the variable is created.

 

文档指出,当在函数声明中声明变量时,变量将作为函数局部变量(准确来说,挂在与该函数相关联的执行环境中变量对象下成为其属性),否则,他们将成为全局变量。变量是在执行流进入函数执行环境时被创建的。文档同时指出,ECMAScript没有块级作用域的概念。只有程序(各种Statement与FunctionDeclaration的集合)或函数声明能创建一个新的作用域。

变量创建后默认被初始化为undefined,如果提供初始化器,则在变量声明被执行的时候赋值(笔者认为赋值的时间点在创建时或在执行时,对编码影响不大,FIXME)。

 

值得一提的是,文档所提到的variable statement,是不能缺少var关键字的,隐藏含义即,缺少var,即使定义(严谨来说不算定义,理解为不声明而直接初始化之)在函数声明中,也是全局变量。给出相关语法:

 

Syntax 写道
VariableStatement :
var VariableDeclarationList ;

VariableDeclarationList :
VariableDeclaration
VariableDeclarationList , VariableDeclaration

VariableDeclaration :
Identifier Initialiseropt

Initialiser :
= AssignmentExpression

来看一些demo

var n0;//初始化为undefined
var n1 = 1;//全局变量
n2 = 2;//全局变量
//window.n2=2;//与上一行等价,但IE部分版本不支持
//n2;//错误语法,抛出ReferenceError,必须提供初始化器,或提供var关键字
function demo() {
	var n3 = 3;//局部变量
	n4 = 4;//全局变量,不推荐这样定义
}
alert(n0);//undefined
alert(n1);//1
alert(n2);//2
demo();//别漏掉这行,否则n4不会创建
//alert(n3);//抛出ReferenceError
alert(n4);//4
alert(window.n4);//虽IE不支持,但每个版本都可以通过它来取值

 

再补充一些demo

全局变量的属性是无法删除的(后来发现局部的也是如此)

 

var n1 = 1;
delete n1;//无效
//delete window.n1;//IE不支持,证明其{DontDelete}特点,chrome支持,但都无效:(
alert(n1);//1

//顺便熟悉一下delete语法
var r1 = new Object();//reference
r1.name = "jack";
alert(r1.name);//jack
delete r1.name;
alert(r1.name);//undefined

var p1 = "hello";//primitive
p1.name = "mike";//不可在原始类型上动态添加属性,不会导致错误但无效
alert(p1.name);//undefined

为了或得更好的页面性能,可尝试尽早的释放全局变量,尤其是引用类型的全局变量。(置为null,解除引用),否则只能依赖垃圾回收或应用的关闭了。至于局部变量,因为在执行流退出其执行环境的时候,环境本身会释放,所以不用操心。

 

一些猜想:即使能delete也只能导致其值为undefined,而垃圾回收应只针对空指针引用(null)而非undefined,大概这是ECMA-262从第三版引入Undefined类型的原因之一,正式区分空指针和未初始化变量,便于更有效的管理内存。FIXME

 

文档中指出全局变量不可delete,是否局部的可以呢(尽管这样实在没什么必要。。),笔者YY了一些代码,仅供参考:

 

function demo() {
	var n = 1;
	delete n;//无效。。
	delete arguments.callee.n;//也无效。。。但都不会报错
	alert(n);
}
demo();

当执行流进入函数时,即进入某一个执行环境,该环境关联一个变量对象(variable object),该对象以property形式保存在该环境中声明的所有变量及函数,换句话说,函数中声明的所有变量其base object都是variable object,根据delete操作符的语法,找到base object才有可能删除其下的属性,虽然ECMA-262并未明文指出variable object的明确定义,但在文档中多处提到“using xxx as the variable object”,上面这个demo中的callee就是持有arguments的对象,但删除失败,只能证明这个variable object和其关联的activation object(活动对象)是无法通过代码引用的。FIXME

2、弱类型 

ECMA-262 Language Overview 写道
ECMAScript syntax intentionally resembles Java syntax. ECMAScript syntax is relaxed to enable it to
serve as an easy-to-use scripting language. For example, a variable is not required to have its type declared
nor are types associated with properties, and defined functions are not required to have their declarations
appear textually before calls to them.

ECMAScript语法类似Java,但语法较为松散。变量在声明时无需指定类型,被定义函数也无需在调用前进行声明(像C语言那样)

 

语法的松散特点之一体现在变量不光是值,其类型也可以在生命周期中改变(不推荐反复更改一个变量的值类型)

 

变量包含2种:原始类型(Undefined,Boolean,Null,Number,String) 和引用类型

原始类型保存于栈内存,占有固定大小空间,具有索引速度快的特点

引用类型为保存于堆内存中的对象,大小不固定,索引相对较慢,变量实际上保存的是指向某对象的一个指针

 

不同类型变量,操作方式也不一样,下面仅浅析部分特点(FIXME):

原始类型在对变量进行复制时,会在栈内存中创建一个新值交由新变量持有,而引用类型,则是在栈内存中复制指针地址值,交由新变量持有,但此时,堆中对象仍然保持一个不变,即2指针指向同一对象。

var n1 = 1;
var n2 = n1;
n1 = 2;
alert(n2);//2,不受n1影响

var r1 = new Object();
r1.name = "jack";
var r2 = r1;
alert(r2.name);//jack
r1.name = "mike";
alert(r2.name);//mike,受r1影响

2、浅析JavaScript变量作用域

在谈及变量作用域的时候,execution context(执行环境)是一个非常关键的概念,并且相关伴随很多内部机制,理解其工作原理,能帮助我们更好的识别变量的行为和状态。

ECMA-262 写道
When control is transferred to ECMAScript executable code, control is entering an execution context. Active
execution contexts logically form a stack. The top execution context on this logical stack is the running
execution context.
......
Every execution context has associated with it a variable object. Variables and functions declared in the
source text are added as properties of the variable object. For function code, parameters are added as
properties of the variable object.
......
Every execution context has associated with it a scope chain. A scope chain is a list of objects that are
searched when evaluating an Identifier. When control enters an execution context, a scope chain is
created and populated with an initial set of objects, depending on the type of code. During execution
within an execution context, the scope chain of the execution context is affected only by with
statements (see 12.10) and catch clauses (see 12.14).
 

 

当执行流进入一段可执行代码时,便进入一个执行环境,活动的执行环境组成一个环境栈,栈顶端的执行环境即为当前正在运行的环境。当环境退出时,相关资源也会得以释放。

 

我们知道JavaScript是一个基于对象的语言(object-based),那么对于变量及函数的管理也是基于这个特点。每个执行环境都会关联一个变量对象(variable object),变量对象是在进入某一个环境的时候进行创建并初始化的,所有在环境中声明的变量和函数都会作为其property挂载在这个对象上,包括函数中的参数。

 

那么我们所关注的变量,其在使用过程中是如何被访问,当遇到同名变量时,如何对其进行追溯并确认的呢?在其内部,是通过一个名为作用域链(scope chain)这样的线性数据结构来保证对环境中所有变量和函数的有序访问的。作用域链是在执行流进入一个执行环境的时候进行初始化的,里面填充的对象,根据进入的代码类型不一样,会产生动态变化,但大多数填充的都是每个执行环境当中的变量对象,并且,在作用域的前端保存的为当前正在运行的执行环境的变量对象(也有一个说法,称之为活动对象activation object)。文档同时指出,还可以通过2种方式来改变作用域链的格局,with和try-catch。

 

对标识符的解析,是一个沿着作用域链从左至右的搜索过程,直至找到标识符并返回为止。就变量的可见性而言,作用域链成为当前环境可以访问到的变量的严格范围,即如果访问一个当前环境所关联的作用域以外的变量,则会抛出类似引用不到的错误(注意,不是undefined)。FIXME

 

ECMA-262 写道
Every function and constructor call enters a new execution context, even if a function is calling itself
recursively. Every return exits an execution context. A thrown exception, if not caught, may also exit one
or more execution contexts.
When control enters an execution context, the scope chain is created and initialised, variable instantiation
is performed, and the this value is determined.
The initialisation of the scope chain, variable instantiation, and the determination of the this value depend
on the type of code being entered.

文档对何时会进入一个新的执行环境进行了明确的定义:

函数和构造器的调用,包括函数的递归调用,都会导致进入一个新的环境,return则退出,如果抛出异常,无catch的情况下也会导致进入新环境。

 

好了,将这段信息结合上文理解,那么问题来了,读者可能会提出这样的大哉问:不同的执行环境都关联着这样一个作用域,那么,在某一个环境中所能访问到的变量,随着执行流的forward,是如何因执行环境的不同而产生变化的?更进一步的说,环境所占用的资源,又是怎样释放的?要分析这个问题,对作用域在不同时期状态的理解,将成为解决问题的关键。

 

下面我们一起来看2个demo,笔者主要结合环境栈和作用域来尝试进行分析:

var n1 = 1;
alert(n1);//mark1
function first() {
	var n1 = 2;
	var n3 = 3;
	alert(n1);//mark2
	second();
	//alert(n4);//mark4
}
function second() {
	var n4 = 4;
	//alert(n3);//mark3
}
first();
alert(n1);//mark5

第一步:当执行流进入全局执行环境(对web浏览器来说,我们一般认为是window对象)的时候,会创建该环境的一个变量对象,在这个变量对象中,保存n1及2个函数声明。这时window执行环境会被压入环境栈,并根据该window执行环境的变量对象初始化作用域链。

环境栈:

window执行环境
......

 

 

 

 

作用域链:

window变量对象

 

 

那么在代码mark1处n1的值为1。刁钻的读者可能会问,为什么不是在first函数中定义的n1=2呢,first函数不是也有一个相应的变量对象吗?要知道,虽然在window执行环境中存在2个函数的定义,并且他们也确实保存在window执行环境的变量对象下,但此时执行流并未进入first函数的执行环境,所以与first函数执行环境对应的变量对象此时还并未创建。

 

第二步:执行流在运行至倒数第二行时进入first函数执行环境,并将此环境执行压栈操作,因为并未退出window对象,所以此时环境栈自顶向下依次为first环境、window环境。

first执行环境
window执行环境
......

 

 

 

 

 

 

对作用域链插入新的变量对象时,是从前端操作的。此时它演变成:

first变量对象

window变量对象

 

 

在mark2处对同名标识符n1的解析,将从first变量对象(作用域链的前端)中开始查找,此时找到n1的值为2并返回,也就不会再在作用域链的下一个(window变量中)查找了。

 

第三步: 此时,在first函数内部对second函数进行调用,执行流离开first执行环境并进入second执行环境,程序对first环境执行出栈操作,然后对second环境执行压栈操作,因为没遇到first函数结束符“}”,first环境虽然出栈,但所占用资源仍然不会释放。

second执行环境
window执行环境
......

 

 

 

 

 

 

而此时,作用域链则产生如下变化,首先first环境的出栈,将会导致从前端移除first变量对象,然后将当前活动的执行环境(second环境)的变量对象添加在作用域前端

second变量对象 window变量对象

 

 

那么在mark3处如试图对在first环境中定义的变量n3求值,则会导致错误(不是undefined),因为此时n3在second变量对象和window变量对象中,都找不到定义。

读者此时应该注意到一个规律,环境栈中的顺序与作用域链中的顺序是一致的。除此之外,我们说,在环境栈中,下方的环境相对于上方相邻的这个环境来说为包含环境,换言之,在一个环境中能访问到的变量,除开自身环境的变量定义外(优先查找,因为在作用域链前端),还有来自包含环境及其级联包含环境的。

 

第四步:执行流退出second环境,返回first环境,相应的second环境出栈,first环境压栈,由于执行流遇到second函数结束符“}”,second环境在出栈的同时,所占用的资源也得以释放。

first执行环境
window执行环境
......

 

 

 

 

 

 

相应的作用域链移除second变量对象,然后在前端插入first变量对象,变为

first变量对象 window变量对象

 

 

在mark4处试图访问second环境中定义的n4变量也是会导致错误的(过程不难分析,略)

 

第五步:执行流离开first环境, 回到window环境,first环境出栈,同时释放其所占资源,作用域链移除前端first变量对象,只留下window变量对象。mark5处n1值为1。(图、分析略)

 

再来看另一个demo

 

var n1 = 1;
function outter() {
	var n1 = 2;
	alert(n1);//mark1
	function inner() {
		var n3 = 3;
		alert(n1);//mark2
	}
	inner();//mark0
	//alert(n3);//mark3
}
outter();
alert(n1);//mark4
 

 

第一步:执行流运行至mark1时,n1值为2。分析同上。环境栈及作用域链状况如下

outter执行环境
window执行环境
......

 

 

 

 

 

 

outter变量对象 window变量对象

 

 

第二步:在mark0处对inner函数的调用,使执行流进入inner执行环境,但此时,并不会导致outter执行环境的出栈,仅仅是在环境栈原有2个执行环境的基础上继续对inner环境执行压栈,因为当前活动的执行环境(inner),其函数是声明在outter环境中的,即这个inner函数本身就是作为outter环境的变量对象中properties当中的一员。这一点与上例有着本质上的区别(上例,2个函数之间的关系为并列关系,都是window变量对象的一员)。环境栈及作用域链状况如下

inner执行环境
outter执行环境
window执行环境
......

 

 

 

 

 

 

 

inner变量对象 outter变量对象 window变量对象

 

 

在mark2处,n1值为2,在outter变量对象中找到(分析略)

 

第三步:在mark3处,尝试访问n3会报错(图、分析略)

 

第四步:在mark4处,n1值为1(图、分析略)

 

最后补充一个带with的demo:

 

var n1 = 1;
function outter() {
	var n1 = 2;
	function inner() {
			var n1 = 3;
	}
	inner();
	with (n1) {
		//var n1;//mark3
		//var n1 = 4;//mark2
		var n2 = n1 + 4;
	}
	alert(n2);//mark1
}
outter();

看读者能否自行分析出,mark1处n2值为6的结果,提示:

1、with/try-catch语句,都会在作用域链的前端添加一个变量对象(with后所跟参数的所有属性、方法所作的变量声明)

2、执行流进入with后所跟的“{}”块,并不会导致进入一个新的执行环境,因为没有块级作用域一说,所以在这个块中声明的变量会被添加在所在执行环境的变量对象中。

 

读者可尝试把mark2和mark3处代码交替取消注释运行查看结果。

 

关于变量及其作用域的分析,笔者认为在大多数时候都是比较直观简单的,没必要如本文中如此这般细掰,但是对于程序错误的分析定位,闭包分析,内存管理,则尤为重要。对于复杂抽象的技术(设计模式)我们应该善于转变成简单的案例进行理解把握,但对于基本功,则相对应该深入探究。

 

关于本文所涉及的参考,读者大多部分都可以从ECMA-262第十章节Execution contexts中找到,good luck

 

分享到:
评论
1 楼 threestone1026 2011-03-08  
太棒了,看了有恍然大悟的感觉。

相关推荐

    javascript变量作用域

    JavaScript 变量作用域详解 JavaScript 变量作用域是基于其特有的作用域链的。在 JavaScript 中,变量作用域是指变量可以被访问和修改的范围。 JavaScript 没有块级作用域,而是基于函数作用域和全局作用域的。 ...

    理解JavaScript变量作用域.pdf

    理解JavaScript变量作用域.pdf 本人还有几十本经典javascript书籍以及无数javascript资料,要的加我qq 568094881,本人网址:www.maoshanhai.com

    基于JavaScript的变量作用域的辨析.pdf

    基于JavaScript的变量作用域的辨析 本文探讨了JavaScript中的变量作用域问题,通过对两种变量的特性从不同角度进行分析和测试,讨论了如何控制变量作用域的有效方法。 变量作用域是JavaScript编程过程中经常遇到的...

    js变量作用域

    ### JavaScript 变量作用域详解 #### 一、引言 在探讨JavaScript的面向对象特性之前,理解变量作用域的概念至关重要。本篇文章旨在通过一系列示例和解释,帮助读者掌握JavaScript中的变量作用域机制。 #### 二、...

    深入理解变量作用域

    本文将从JavaScript权威指南出发,深入探讨变量作用域的相关知识点,包括全局作用域、局部作用域、以及闭包等高级概念。 #### 二、全局作用域与局部作用域 1. **全局作用域** - 定义:在JavaScript中,如果一个...

    JavaScript变量的作用域全解析

    变量作用域是程序中定义这个变量的区域。 先来看一段示例: /* 代码1 */ var scope = "global "; function checkScope() { var scope = "local "; function childCheck() { var scope = "childLocal "; ...

    JavaScript 基础函数_深入剖析变量和作用域

    JavaScript是一种广泛用于网页开发的脚本语言,它的函数和作用域是实现代码逻辑结构和模块...理解JavaScript中的变量作用域非常重要,它有助于避免变量命名冲突和意外的变量修改,从而编写出更加健壮和易于维护的代码。

    JavaScript程序设计-变量作用域.pdf

    JavaScript是一种广泛用于网页和网络应用的编程语言,其核心特性之一就是变量作用域。变量作用域决定了变量在何处可以被访问以及何时会被销毁。本文将深入探讨JavaScript中的全局变量、局部变量、变量提升、词法作用...

    JavaScript程序设计变量作用域共4页.pdf.zi

    本文将深入探讨JavaScript中的变量作用域,以及它如何影响代码的运行和组织。 首先,我们来理解什么是变量作用域。在JavaScript中,变量作用域决定了变量在何处可以被访问和识别。主要有三种类型的作用域:全局作用...

    JavaScript 学习笔记之变量及其作用域

    在作用域分析的例子中,我们看到了变量作用域的另一个有趣方面。看下面这个例子: ```javascript var i = 0; // 定义全局变量i function test() { demoFunction(i); function innerFunction() { i = 1; // 局部...

    【JavaScript源代码】JavaScript高级程序设计之变量与作用域.docx

    JavaScript是一种广泛应用于网页和网络应用的脚本语言,它的核心特性之一是变量和作用域的概念。在深入探讨这些概念之前,我们先理解一下原始值和引用值的区别。 1、原始值与引用值 JavaScript中有六种原始数据...

    深入理解JavaScript作用域和作用域链

    JavaScript作用域是编程中至关重要的概念,它规定了变量和函数的可见性及生命周期。JavaScript主要有两种作用域:全局作用域和局部作用域。 全局作用域是指在代码的任何位置都可以访问的变量或函数,这通常包括在最...

    Javascript变量的作用域和作用域链详解

    JavaScript中的变量作用域和作用域链是编程中非常重要的概念,尤其对于JavaScript这种函数作用域的语言来说更是如此。本文将详细解析这两个知识点,并通过实例帮助理解。 **一、变量作用域** JavaScript中的变量...

    Javascript的词法作用域分析.pdf

    Javascript中的词法作用域分析是指变量的作用域不是在执行时决定的,而是在定义时决定的,也就是说通过静态分析就能确定。因此,词法作用域也叫做静态作用域。 在Javascript中,一个方法的词法作用域是指该方法在...

    一道关于JavaScript变量作用域的面试题_.docx

    JavaScript变量作用域是编程语言中一个非常核心的概念,它决定了变量在何处可被访问以及其生命周期。在这道面试题中,我们看到的是如何在不同作用域下声明和使用变量,特别是与for循环相结合的情况。 首先,我们要...

    javascript执行环境,作用域理解

    执行环境是一个概念,一种机制,用来完成 JavaScript 运行时在作用域、生存期等方面的处理,它定义了变量或函数是否有权访问其他数据,决定各自行为。 一、执行环境(Execution Context) 在 JavaScript 中,所有...

    理解JavaScript变量作用域更轻松

    JavaScript变量作用域是编程基础中的重要概念,它决定了变量在哪些范围内可以被访问。了解变量作用域可以帮助开发者更好地管理代码中的数据,防止意外的变量冲突,从而提高代码的稳定性和可维护性。 JavaScript中...

    第12章 变量、作用域及内存1

    首先,我们要了解变量及其作用域。JavaScript中的变量是松散类型的,这意味着它们在声明时不需要指定数据类型。变量可以保存基本类型或引用类型的值。基本类型包括Undefined、Null、Boolean、Number和String,它们在...

    JavaScript 基础函数_深化剖析变量和作用域_.docx

    在JavaScript编程语言中,变量和作用...总的来说,理解JavaScript中的函数定义、调用、参数处理、变量作用域和安全实践是编写高效、安全代码的基础。掌握这些概念可以帮助开发者编写出更加灵活和可靠的JavaScript代码。

Global site tag (gtag.js) - Google Analytics