[译文-上半部分]
Closure介绍
Js闭包是一种封闭代码块(一般来说是一个函数),它包含了自由变量和绑定这些自由变量的环境,这些变量不是在这个代码块或者全局作用域定义的,而是在定义代码块的环境中定义。
闭包是Javascript中最强大的特性之一,但是在完全理解它之前这种强大的功能很难能发挥出来。创建闭包相对而言比较简单,有时甚至是意外的创建了闭包,而意外闭包的创建往往存在潜伏的危害,特别是当它存在于浏览器的公共环境中。为了避免意外的引入有危害的闭包、而又要受益于闭包的强大功能,就有必要了解它的机制。首先要了解的是作用域链(Scope Chain)在Javascript如何定位变量的、而每个对象的property值又是如何解析的。
另一种对Js闭包的理解是:Javascript允许内部函数,即函数的声明定义是完全被包含在另外一个函数的函数体之内。这个内部(Inner)函数有权限访问自身和它外部(Outer)函数所有的local变量、parameter值(Js没有块级作用域)。当这个内部函数在包含它的Outer函数之外仍然可以访问的时候,一个闭包就形成了。这个时候所有Outer函数的local变量、parameter值包括Inner函数的声明也是处于可访问状态。这些变量和Inner函数声明是保持上一次Outer函数调用返回时候的值。
对象的属性名解析
ECMAScript(Javascript)识别两种类型的对象,Native对象和Host对象,Native对象也有另外一种名称叫做Built-in(内置)对象。内置对象来源于Js语言本身,而Host对象是运行环境中产生的,比如document对象和DOM各节点对象等。
内置对象的属性名命名很宽松,每个命名的属性具有一个值,可能引用另外一个对象也可能是一个基本类型数据值,这里函数也属于对象,基本数据包括String、Number、Null or Undefined。这个Undefined或许有些奇怪因为具有Undefined值的属性其实有定义的(至少该命名的属性是存在),只不过具有的值是Undefined而已。当一个属性被赋值时,如果取属性的对象并没有定义这个属性,一个同名新的属性就会被定义并完成赋值,如果已经定义就执行re-set操作。
读取值的时候由于每个对象都有原型对象prototype属性,而这个prototype本身也是一个对象,对象就可以绑定属性值,当这个prototype对象上含有要读取的同名属性时,它的值就会被返回作为读取结果。那么既然prototype也是对象,对象又会具有它的prototype,这样就会形成一个原型链。当原型链中某个对象拥有一个为null的prototype属性时原型链就终结。默认情况下Object的构造函数具有一个null值的原型链。所以var ref= new Object() 将会创建一个对象,它具有的原型对象Object.prototype值为null。
function MyObject1(param1){
this.testNumber = param1;
}
function MyObject2(param2){
This.testString = param2;
}
MyObject2.prototype = new MyObject1(8);
var ref = new MyObject2(“String value”);
MyObject2的这个实例ref拥有一个原型链。链上第一个对象是一个MyObject1的实例对象,这个对象创建出来然后赋值或者说挂载在MyObject2的原型对象上。而MyObject1有自己的原型链,链上仅有一个对象就是默认的Object.prototype,由于它已经为null原型链的解析终结。当从ref从读取testString值时它直接从MyObject2中找到改属性并返回;当读取testNumber时需要遍历到原型链中的MyObject1实例上才能找到匹配的属性,当读取toString属性时就遍历到了Object基类的原型对象上才找到匹配属性。如果都无法匹配则返回undefined。
读取值的顺序是依次读取对象自身域、然后是原型对象域、然后原型域对象的原型域,递归下去直到为原型属性为null,遇到第一个匹配的属性值就返回结果。
执行上下文(Execution Context)
执行上下文(Execution context)是一个抽象的概念,用来定义Javascript脚本执行的环境。所有Js代码将在上下文中执行,全局Js将在一个叫做全局上下文的环境中执行,所有函数调用(包括构造对象)将会产生一个函数上下文,由eval调用执行的Js也会关联到一个特定的执行上下文中(虽然几乎部怎么使用)。
当一个Js函数被调用时就进入了它对应的函数上下文,当在这个上下文中另外一个函数被调用或者本身函数被二次调用时,一个新的函数上下文又会产生。这个函数上下文一直持续存在到函数调用结束。执行环境将会退出到第一个函数上下文中,于是这里就形成了一个执行上下文的栈式结构 (调用栈)。
当一个函数上下文被创建的时候,一系列事情按顺序发生。首先,在函数上下文中有一个叫做”Activation Object”的对象被创建出来,它属于另外一种创建机制因为它具有某些命名属性类似对象但又不具有prototype属性并且无法被Js代码直接引用。下一步,在函数上下文中创建arguments对象,一个类似数组的结构,可以按照整数下标顺序取出里面的元素。它具有length和callee属性。之前的Activation Object有一个同名为arguments的属性将作为arguments对象的引用。再下一步,这个函数上下文会被赋值或者说挂载到一个作用域。一个作用域包含了一连串对象。每一个函数对象也将包含内置作用域属性包含着一连串的对象,即函数关联的所有属性值,函数作用域链最终被挂载到函数上下文上,而Activation Object就作为这条作用域链的第一个元素。
再下一步将是变量初始化的过程,这个时候Activation Object充当一个变量载体的角色,称之为”Variable Object”,根据函数参数名命名相应的属性,如果参数与arguments数组元素正好数量对应则依次赋值,否则若arguments过长则多出来的值没有属性名,如果arguments元素不够则有些参数会赋值undefined。内部函数会以它声明的名字作为属性名存储到Variable Object上,最后一步初始化就是针对所有的local变量根据属性名赋初始化值为undefined,所有的local变量只有在真正调用时才会根据实际情况被赋值。从Activation Object和Variable Object其实是同一个对象的角度来理解,arguments也成了一个local变量。
终于函数上下文创建完毕,之后所有的变量访问都必须使用this关键字,即使没有显式也会被访问器自动加上this前缀。如果属性属于Object即要么有值要么为undefined则this引用这个对象实例,若为null则说明该对象不包含这个属性this引用全局对象。
全局上下文比较特殊,它不存在arguments属性所以也不需要定义Activation Object,它也不需要一个作用域对象,因为它的作用域里永远只有一个对象就是全局对象自身。它也不必执行变量初始化的过程,对于全局对象来说所有的inner函数其实就是最上层声明的普通函数而已。不过它同样会使用this关键字指向这个全局对象,它仅仅简单的被当作一个变量载体来使用,所以所有的顶层函数和全局变量其实都是这个全局对象的属性之一。
作用域链(Scope Chain)
函数调用时作用域链是通过挂载/添加执行上下文中Variable Object作为头节点来构造的,本身它也作为函数对象的一个属性。由于Javascript中函数也是对象,它们在扫描函数声明的变量初始化阶段就已经被创建,创建时候调用了Function的constructor,这里的Function是指Js的一个内置对象。通过这个构造器创建的函数对象都会有一个属性指向作用域链初始化包含唯一的全局对象。
当构造唯一的全局对象时,扫描所有的顶层函数并完成对全局对象的作用域链属性解析,所有顶层函数对象的作用域链也是在这个时候形成,并且都会带有全局对象的引用。
内部函数会造成函数对象的构造在另外一个函数上下文环境中,这就使得它的作用域链对象更加复杂。考虑一个例子况:当一个Outer函数被调用时,会创建一个新的函数上下文环境,这个上下文环境的作用链包括一个新的Activation Object和函数对象本身的作用域链(scope)属性。在变量初始化阶段一个内部函数对象被创建并带有自身的作用域链属性,这个内部函数对象的作用域链依次挂载了此函数的Activation Object和全局对象。到目前为止,这一系列动作都是在源代码和Js脚本语言机制下自动完成的,执行上下文的作用域链属性定义了所有被创建的函数对象,而函数对象的作用域链属性定义了它们被调用时启动新的上下文环境的作用域。ECMAScript提供了一个”with”语法来意图修改作用域链。
with语句接受一个表达式参数,如果表达式是一个对象,它将会被挂载到当前执行上下文环境的作用域链上来,并且位置位于Activation Object之前。接下来with执行一个块语句,执行完毕会还原之前的作用域链。函数声明无法在with语句块中发挥作用,因为它仅仅创建了一个函数对象不存在执行环境,但函数表达式可以在with语句块中被执行。
标识解析(Identifier Resolution)
标识的解析正好跟作用域链相反,ECMA规范把this当作一个关键字而不是标识是不太合理的,因为每次都是在执行上下文中都是依赖this来解析标识而不是通过作用域链。标识的解析从作用域链的第一个元素开始,先检查第一个作用域中是否存在这个标识,因为作用域链上的都是一个个对象,所以这种检查也会覆盖对象的原型链。如果第一个作用域对象上无法找到标识同名的属性名就检查第二个,依次遍历下去直到直到该属性或者作用域链条终结。对已经找到解析的标识进行操作会跟操作对象的属性遵循一样的流程(读/写)。
函数被调用时关联上的执行上下文将会把Activation Object放在作用域链的第一个位置,标识解析也会首先检查Activation Object对象的属性,看是否能在函数参数、内部函数名、local变量中找到对应的名字。
[End]
下半部分会在近期翻译完毕,个人觉得讲解非常透彻,确实是学习Js的一篇好资料。