转自:http://ghsky.com/2010/11/deep-into-javascript-closure-part-two.html
继续上次闭包的经典文章Javascript Closures的研读。这次学到了“作用域链与函数内部[[scope]]”一节。继续以原文翻译+自己理解的方式呈现文章内容。
正如上节提到的那样,一个函数调用会创建一个执行上下文,而这个执行上下文中包含一个作用域链。在这个作用域链的最开始部分是执行上下文的Activation/Variable对象,后面紧接着就是函数对象自身的[[scope]]
属性,因此很有必要去了解函数内部的这个[[scope]]
属性如何定义的。
在ECMAScript规范中,函数实际也是对象。这些函数对象要么是“函数申明(function declarations)”方式的函数——在变量实例化阶段创建的,要么是“函数表达式(function expressions)”——在执行阶段创建的,要么是“函数构造器(Function
constructor)”——在调用实例化时创建。
这里比较特殊需要注意的是,使用“函数构造器(Function
constructor)”创建的函数对象的[[scope]]
属性只包含全局对象。
而使用“函数申明(function declarations)”或者“函数表达式(function expressions)”创建的函数对象,其[[scope]]
属性包含了创建该函数的那个父函数(或者全局对象)的执行上下文(译者注:可能这里有些绕,不大容易理解,不过下面会有详细解释,这里大概有这样一种概念就好)。
例如最简单的全局函数声明:
1
2
3
|
function
exampleFunction(formalParameter){
// function body code
}
|
在全局执行上下文的变量实例化阶段会创建该函数对应的函数对象。全局执行上下文中有一个只包含一个全局对象的作用域链。因此创建出来的函数对象会以“exampleFunction”的属性名添加到全局对象中,并且为这个函数内部的[[scope]]
属性指向只包含全局对象的作用域链(译者注:也就是当前的全局执行上下文)。
类似地,在全局中使用函数表达式声明函数:uoy
1
2
3
|
var
exampleFunction =
function
(formalParameter){
// function body code
}
|
与上例有一点儿不同的是,具名属性“exampleFunction”在全局执行上下文的变量实例化阶段创建,但是其并不指向任何函数对象,因为此时还为创建函数对象(译者注:通常这是由于JS解析引擎将var声明的变量语句提前执行,导致了变量声明和赋值是两个阶段完成)。直到代码执行到真正的函数表达式赋值语句时,才会创建这个函数对象,并让具名属性“exampleFunction”指向它。尽管函数对象创建的时间“较晚”,但其创建依然实在全局之下上下文中完成,因此其[[scope]]
属性依然指向当前全局执行上下文,其中只包含一个全局对象(译者注:这一关键点是与上例相同的,因此他们的作用结果就是[[scope]]
属性实际指向相同,因此也就能解释两种函数声明方式的类似,其实当了解了这些原理之后再来看JavaScript的各种函数定义,获取会有更深地理解)。
那么对于在函数内部的函数声明或者函数表达式,它们的函数对象创建是在外部函数的执行上下文中完成,因此将会获得更“丰富(elaborate)”的作用域链。考虑如下代码,我们在一个函数内部再声明了一个函数,然后执行外部这个函数:
1
2
3
4
5
6
7
8
|
function
exampleOuterFunction(formalParameter) {
function
exampleInnerFuncitonDec() {
// inner function body
}
// the rest of the outer function body.
}
exampleOuterFunction(5);
|
正如上面说到的那样,外部函数声明所对应的函数对象是在全局执行上下文的变量实例化阶段创建的,因此它的[[scope]]
属性包含只有唯一对象(全局对象)的作用域链。
当全局代码执行到exampleOuterFunction
的函数调用时,自然会为这次调用创建一个新的执行上下文(译者注:我们把它称作执行上下文A),同时包含一个Activation/Variable对象在其中。新创建的执行上下文A的作用域链包含新创建的Activation对象,之后紧接着外层函数(即exampleOuterFunction
)对象的[[scope]]
属性(只包含一个全局对象)。新执行上下文A的变量实例化阶段,将会为内层函数定义创建与之对应的函数对象,当然也会为这个函数对象赋值[[scope]]
属性,其指向创建这个函数对象的那个执行上下文A(译者注:需要特别注意这里的逻辑关联),因此这个内部函数的[[scope]]
属性是一个“包含Activation对象,之后紧接着全局对象”的作用域链。
至此以上所有都是自动地、受结构控制地执行程序源代码。执行上下文的作用域链决定了其中创建的函数对象的[[scope]]
属性,同时函数对象的[[scope]]
属性决定了该函数被调用时的执行上下文(自然还包括执行上下文中与之对应的Activation对象)。但是ECMAScript提供的with
语句却是可以修改作用域链的一种方法。
with
语句计算一个表达式,如果表达式是一个对象,那么就将这个对象添加到当前执行上下文的最前端(在Activation/Variable对象之前)。接下来with
语句执行剩余的语句(当然也可能出现其中又包含一个自己的语句块),之后还原执行上下文的作用域链回到之前的状态。
函数声明是不会受到with
语句的影响,因为它们的函数对象实在变量实例化阶段创建的,因此此时没有with
语句的影响。但是对于函数表达式就不同了,它们可能在一个with
语句块中执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/* create a global variable - y - that refers to an object:- */
var
y = { x: 5 };
// object literal with an - x - property
function
exampleFuncWith() {
var
z;
/* Add the object referred to by the global variable - y - to the
front of he scope chain:-
*/
with
(y){
/* evaluate a function expression to create a function object
and assign a reference to that function object to the local
variable - z - :-
*/
z =
function
() {
// inner function expression body;
}
}
}
/* execute the - exampleFuncWith - function:- */
exampleFuncWith();
|
当exampleFuncWith
函数被调用的时候自然会产生一个新的执行上下文,其中的作用域链包含Activation对象之后紧接着全局对象。当执行到with
语句的时候,会将全局变量y
添加到作用域最前,这时候恰好碰到一个函数表达式的执行。函数表达式执行产生的函数对象其中的[[scope]]
属性被赋值为当前创建其的执行上下文,那么显而易见,由于刚刚所说的,现在的执行上下文中包含y
,且其位置还要在Activation
对象之前,此时作用域链的排列是这样的:y –> Activation –> global object
,因此这个函数表达式就受到with
语句的影响。
当with
语句相关的语句块都执行完毕之后,当前的执行上下文就会被还原(y
对象被移除),但是当时(即在with
语句块中时)创建的函数对象的[[scope]]
属性指向了包含y
对在最前的作用域链。
最后依然是用一张图做总结,下图是对例3的形象描述:
从图中很容易发现,如果在不存在with
语句时,函数对象的[[scope]]
属性与外层函数(或者全局对象)的执行上下文有着密切的联系,这种联系也就形成了作用域链,建立了内外函数的沟通桥梁,这样也便很容易理解为什么内层函数可以访问外层函数局部变量,反之则不行。
- EOF -