欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

(转)深入理解闭包

程序员文章站 2022-07-15 10:26:52
...

这边文章主要是Javascript Closures 这篇文章的学习笔记,可能包含一定的原文翻译,以及一些自己的理解,希望通过这些学习能够更加深入地理解JavaScript中闭包的概念。

下面是The Execution Context一节的翻译与理解。

执行上下文 (The Execution Context)

执行上下文是ECMAScript规范(ECMA 262 3rd edition)中定义的有关ECMAScript实现的相关行为要求。虽然规范没有具体规定执行上下文的实现,但是根据规范中对其所定义的数据结构(包 含一些列相关是属性),因此执行上下文可以理解为(甚至按照)对象的方式来实现,尽管其包含的属性不是公共的。

所有的JavaScript代码都是在执行上下文中所执行的。全局代码(包含已执行的行内代码,JS文件,或者内潜入HTML的JS代码)都是在全局执行上下文中执行的,并且对于每一个函数(包含构造函数)都有一个与之相关联的执行上下文。使用eval 函数执行的代码也是包含一个独特的执行上下文,但是由于其在JavaScript中的特殊性,这里就不再多考虑了。有关执行上下文的具体细则规范可以在ECMA 262 (3rd edition) 10.2节中找到。

当一个JavaScript函数被调用的时候,它就进入到一个执行上下文中。如果其中又有其他函数被调用(或者自身的递归)都会创建一个新的执行上下文,然后函数调用进入到那个执行上下文中,直到那个函数执行结束(译者注:这不就类似盗梦空间 中的进入一层层梦境,直到该层梦境的时间结束,由此看来盗梦空间 真是值得广大程序员好好研究的佳片啊=,=||)。因此,由于这种现象就在代码执行中形成了一个执行上下文堆栈。

当一个执行上下文被创建的时候会依次经历以下几个阶段。首先,在函数的执行上下文中创建一个Activation(激活)对象(貌似犀牛书 中就是这样翻译的~)。Activation对象实际是规范中又一机制。由于它拥有一些可访问的具名属性,因此可以将它作为一个对象来看待,但比较特殊的是,它没有原型对象(至少没有一个定义的原型对象),同时Activation对象也不能在代码中直接引用。

Activation对象创建完毕后,下一步就是为函数调用创建arguments 对象,众所周知,arguments 对象是一个类数组的对象,使用整数对其成员索引,且其排列顺序与函数调用传递的参数顺序一致。同时arguments 对象还拥有lengthcallee 两个成员(译者注:实际还应该有一个caller 成员,不过这个成员已经建议弃用了),不过这里与主题无关就不做详细讨论了。这时候会在Activation对象上创建一个名为"arguments"的属性,且指向arguments 对象。

接下来,执行上下文需要给作用域赋值。这里的作用域就是包含一些列的对象,也就是我们常说的作用域链,注意对“链”的理解。每一个函数对象在内部都拥有一个[[scope]] 的属性(稍候会详细介绍),它也包含一个链式的对象。那么执行上下文的作用域主要包括Activation对象,且其处于作用域链的最顶端,之后就是函数对象对应的[[scope]] 对象。

下一步称为变量实例化(variable instantiation),这时候使用到一个对象,在ECMA 262中称为Variable对象。然后Activation对象实际就是当作Variable对象使用(这里需要注意的是,其实两者指的是同一个对 象)。在这个阶段里,会为函数的每一个形参在Variable对象上创建一个具名属性,如果函数调用传入的参数与形参一致的话,那么就会将参数的值逐一赋 值给Variable对象上的那些属性(否则的话赋值为undefined )。之后对于调用函数的内部函数声明,同样首先会给 这些函数创建函数对象,之后以函数名称为Variable对象的属性名,然后添加对应的函数对象引用。变量实例化的最后一个阶段就是处理调用函数的具备变 量。同样的,将所有调用函数内的具备变量声明添加到Variable对象中。(译者注:此时的Variable对象就完整地包含了形参,内嵌函数以及局部 变量)

这里需要注意的是在变量实例化阶段中,为Variable对象添加局部变量时,所赋值的初值均为undefined (译者注:因为我们知道JS引擎会将以var声明的具备变量提前解析,这就是刚刚所提到的变量实例化阶段所做的事儿,因此提前之后它们的初值都是undefined ),而真正的局部变量实例化(赋值)直到在执行代码体的表达式时才会完成。

事实上,带有arguments 的Activation对象与带有函数局部变量的Variable对象就是同一个对象,因此我们也可以把arguments 标识符当作一个函数局部对象。

最后一个步骤就是对this 关键字赋值。如果this 关键字的值为一个对象的话,那么以this 关键字为前缀的属性访问均是指向那个对象;但如果this 关键字赋值为null 的话,那么this 关键字就指向全局对象。

对于全局执行上下文来说,处理的方式会有些许差异,这主要是因为其没有参数,因此就不需要定义Activation对象。同样的,全局执行上下文需 要作用域,且它的作用域只包含一个对象——全局对象;全局执行上下文也需要经历变量实例化,它内部的函数自然就成为了最顶层的函数声明,包含了大量 JavaScript代码;全局对象被当作Variable对象使用,因此其中的变量声明、函数声明自然就成为了全局对象的成员(译者注:这也就是为什么 我们建议在全局代码中用一个匿名函数包裹内部代码,防止全局对象的污染)。

同时,全局执行上下文中的this 对象指向的是全局对象。

最后以一张图片来结尾。

(转)深入理解闭包
            
    
    博客分类: Javascript开发语言介绍 closurejavascript

继续上次闭包的经典文章Javascript Closures 的研读。这次学到了“作用域链与函数内部[[scope]]”一节。继续以原文翻译+自己理解的方式呈现文章内容。

作用域链与函数内部[[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的形象描述:

(转)深入理解闭包
            
    
    博客分类: Javascript开发语言介绍 closurejavascript

从图中很容易发现,如果在不存在with 语句时,函数对象的[[scope]] 属性与外层函数(或者全局对象)的执行上下文有着密切的联系,这种联系也就形成了作用域链,建立了内外函数的沟通桥梁,这样也便很容易理解为什么内层函数可以访问外层函数局部变量,反之则不行。

 

相关标签: closure javascript