深入理解JavaScript闭包(1)

这边文章主要是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对象指向的是全局对象。

最后以一张图片来结尾。

execution_context

- EOF -

4 Comments For This Post

  1. 这篇文章的确是解释JS闭包的经典。得多看几遍 :biggrin:

    回复该留言

  2. Is this the one that slowing the speed of recursive calling?

    回复该留言

    @Cadina, 递归调用效率低下的原因大致都相同,其他语言中同样会有这样的调用堆栈存在,必然要保存很多函数状态信息其中~

    @ghSky, but they don't always copy the context, they extend it

    @Cadina, 递归的时候,在每次进入函数的时候都是创建一个新的执行上下文,当然涉及到函数对内部作用域的东东这里没讲太多,下篇文章会提到~

  3. 话说这个配图是用什么软件画的呀~ 很漂亮哦~~~ 求软件名~

    回复该留言

  4. HOURS?!?*** Yeah, I'm just going to trust them that cutting it was definitely the right thing to I just saw maybe less than five minutes of extra footage where Rambo was supposed to die, and I thought it looked pretty

    回复该留言

One Trackback For This Post

  1. [...] JavaScript 闭包”: 1, [...]

Leave a Reply

:wink: :twisted: :surprised: :smile: :sad: :rolleyes: :redface: :razz: :question: :neutral: :mrgreen: :mad: :lol: :idea: :exclaim: :evil: :eek: :cry: :cool: :confused: :biggrin: :arrow: