最近发现一个英语文档,详述了js中的属性名解析,标识符解析,作用域链,闭包等相关知识,原文在这
我自己翻译了一部分时间关系正在缓慢的进行,百度知道中有一部分
闭包
剩余部分以后更新
JavaScript 闭包
常见问题介绍:
导言
闭包是一个表达式(通常是一个函数),这个表达式包括自由变量和关联这些自由变量的环境(环境封闭了表达式)。
闭包是ECMAS最具影响力的特性之一,但是,不理解闭包就无法有效的利用它。尽管闭包很容易被创建,甚至是无意中被创建出来,但是闭包的创建也会有一些潜在的不利影响, 特别是在一些相当常见的web浏览器环境下。扬长避短,了解闭包的原理很有必要,这很大程度上依赖于作用域链在标识符解析和对对象属性名解析中的作用。
关于闭包最简单的阐述就是在ECMAScript中,允许内部函数的存在,即存在于其他函数体内的函数声明和函数表达式。这些内部函数可以访问外部函数的所有的局部变量、参数和内部声明函数。当其中一个内部函数可以访问包含它的外部函数,并且在这个外部函数返回后仍可访问该外部函数的局部变量、参数、和内部函数声明,一个闭包就形成了。当外部函数返回的时候,这些局部变量,参数和函数声明的初始值仍然存在,而且可以通过内部函数进行互动。
不过,正确的理解闭包需要了解它背后的机制和很多技术细节。尽管下面的阐述中略过了一些ECMA 262(译者注:JavaScript的一个标准,第五版称为ES5)的指定算法,这些机制还是不可忽略或者轻易简化,那些熟悉对象属性名称解析的人可以跳过那个部分,但是只有那些已经熟悉闭包的人可以跳过以下的部分,并且他们现在就可以不用读这篇文章回去使用闭包了。
对象属性名的解析
ECMAScript支持两种对象:原生对象和主机对象,以及一种称为内置对象的原生对象的子类。原生对象属于语言,主机对象由环境提供,就像文本对象,DOM节点这种。
原生对象是松散的,是命名属性的动态包(虽然通常问题不大,但是当涉及到内置对象时,一些实现并不是那么的动态),一个对象的命名属性会有一个值,这个值可能是对别的对象(在这种情形下,函数也认为是一种对象)的引用,或者一个基本类型的值,基本类型可以是String类型、Number类型、 Boolean类型、Null类型、或者Undefined类型。Undefined类型有一点特殊,给一个属性赋值为undefined是可以的,但是这么做并不是删除了这个对象的该属性,只是该对象保有了一个值为“undefined”的属性。
接下来是对属性值是如何读取和设置的简单描述,我们尽可能的略过内部细节。
赋值
可以通过直接赋值给创建的命名属性或者为已存在的属性重置值来进行属性的命名
var objectRef = new Object(); //create a generic javascript object.//创建一个通用的js对象
属性名为“testNumber”的属性可以这样被创建:
objectRef.testNumber = 5;
/* - or:- */
objectRef["testNumber"] = 5;
即使对象在赋值之前没有testNumber 属性,在赋值的时候就会创建一个该属性,任何后来的赋值不需要创建这个属性,只需要重置就可以了:
objectRef.testNumber = 8;
/* - or:- */
objectRef["testNumber"] = 8;
JavaScript中的对象的原型本身就是一个对象,这个原型也可以拥有自己的命名属性。但是这些情况不会给赋值造成影响。赋值的时候,如果这个对象没有对应名字的对象,那么以这个名字命名的属性就被创建了,如果该对象存在对应名字的属性,那么该属性的值就被重置为被赋予的值。
读取属性值
到了读取属性值的时候,就要使用到原型,如果对象有以属性访问器中使用的名字为名的属性,属性值就被返回:
/*
赋值的时候,如果这个对象没有对应名字的对象,那么以这个名字命名的属性在以下语句后就被创建了:-*/
objectRef.testNumber = 8;
/* 读取属性值:- */
var val = objectRef.testNumber;
/* 现在变量val就保有了刚刚为它赋的值8 */
所有的对象都有原型,原型也是对象,因此原型也可以拥有原型,甚至是很多属性,这就组成了所谓的原型链,当链中的对象有一个空原型的时候,原型链就结束了。默认的object的构造函数的原型就是null,因此:
var objectRef = new Object(); //创建一个通用js对象.
创建一个原型为Object.prototype的对象,而Object.prototype的原型是null,因此objectRef的原型链只包含一个对象:Object.prototype
———————————————更新一波———————————————–
标识符解析,运行环境和作用域链
运行环境
运行环境(execution context )是ECMAScript规范中用来定义ECMAScript实现所需行为的抽象概念。ECMAScript规范并没有提及任何运行环境应该如何被实现的事情,而是针对参考该规范构建的结构规定了运行环境应具备的相关属性,这样这些结构就可以被设计为甚至实现为一个具有属性特征的对象,哪怕这些属性并不是public性质的。
所有的js代码都运行在一个运行环境中。全局代码(在线运行的代码,通常作为一个js文件,或者一个html页面加载)在一个全局运行环境中被执行,每一个方法(可能作为一个构造函数)的调用都有相关的运行环境。在eval方法中运行的代码也有一个不同的运行环境,但是鉴于eval不经常被js编程者使用,本文就不做过多的讨论了。运行环境的明确细节可以再ECMA262(第3版)的10.2部分找到。
当一个js方法被调用的时候,他就进入了运行环境。如果在此过程中另一个方法被调用(或者同一函数进行递归),那么一个新的运行环境就被创建了,并且函数在运行期间一直处于该运行环境中,直到被调用的函数return才会返回到原先的运行环境中去。因此运行中的js代码形成了一堆运行环境。
当一个运行环境被创建时,一系列的事情按序发生:首先,在一个方法的运行环境中,一个活动对象被创建。活动对象是另外一种规范机制,由于它以可访问的命名属性结尾,因此可以认为它是一个对象,但是它又不是一个规范的对象,因为它没有原型(至少没有一个定义的原型),并且它也不能被js代码直接引用。
创建调用函数的运行环境的下一步就是创建arguments对象,arguments对象是一个类似数组的对象,它保存着顺序传递进函数中的参数的相关整数索引,具有length和callee属性。活动对象创建一个名为“arguments”的属性,然后将一个指向arguments对象的引用赋给这个属性。
接下来,运行环境被分配进一个域中,这个域由一个对象列表(或者是对象链)构成,每个function对象有一个内部的[[scope]]属性,这一属性也涉及到对象列表。分配给函数调用运行环境的的域包含了对象列表,这个列表就是由相应函数对象的[[scope]]属性指向的添加到这个链的前面(或者列表的顶部)活动对象构成。
当使用一个ECMA 262中提及的“变量对象”的时候,“变量实例化”这个过程就发生了。但是,往往活动对象被当做变量对象来使用(请注意:活动对象和变量对象是同一个对象)变量对象为每个函数的形参都创建命名属性,如果函数调用的参数与这些形参一致,参数的值就会被赋值给那些属性,否则赋值为undefined,内部函数定义用来创建函数对象,这些函数对象值被与函数声明同名的 变量对象 的属性引用 。变量实例化的最后一个阶段,创建变量对象中对应着函数内部声明的局部变量的的命名属性。
函数内部声明对应的局部变量在所对应的变量对象中的命名属性,在变量实例化的过程中被初始化为undefined ,这些局部变量在函数体代码运行时候相应的赋值表达式出现才会被初始化。
事实上,带有arguments属性的活动对象,和 拥有命名属性的变量对象是同样的对象,该变量对象拥有与函数局部变量对应的命名属性。这样就能够允许arguments标识符可以像函数局部变量一样被处理。
最后,一个值被分配给this关键字,如果这个值指向的是一个对象,以this开头的属性访问器中指向这个对象的属性。如果这个值赋值为null,this指针就指向全局对象。
全局运行环境与局部运行环境有些许不同,就是全局运行环境没有arguments,这使得全局运行环境不需要一个定义的活动对象去引用它。全局运行环境也需要一个作用域,但是它的作用域链实际上只包含了一个对象就是全局对象。全局运行环境也会历经变量的实例化过程,它的内部函数是标准的顶层函数声明,这些函数声明占了js代码的一大部分。全局变量被当做变量对象来使用,这也就是为什么全局声明的函数会就像全局声明的变量那样,变成全局对象的属性值。
全局运行环境也引用了this对象引用的全局对象。
作用域链和[[scope]]属性
函数调用的运行环境的作用域链是由在链头增加运行环境的活动/变量对象组成的,作用域由函数对象的[[scope]]属性保存,所以弄懂[[scope]]是如何定义的十分重要。
在ECMAScript中,函数也是对象,它们在变量实例化的过程中由函数声明创建,或者由函数表达式赋值被创建,或者调用function构造函数被创建
由function构造函数创建的函数对象拥有[[scope]]属性,该属性引用一个只包含全局对象的作用域链。
由函数声明或者函数表达式创建的函数对象,它们的作用域链赋值给它们内部的[[scope]]属性。
举一个最简单的全局函数声明的例子:
function exampleFunction(formalParameter){
... // function body code
}
相应的函数对象在全局运行环境变量实例化的过程中被创建,全局的运行环境的作用域链只有一个全局对象,因此全局对象中名为“exampleFunction”属性值所指向的函数对象的内部[[scope]]属性指向的作用域链只包含了全局对象。
一个小的作用域链在函数表达式在全局上下文运行的时候形成:
var exampleFuncRef = function(){
... // function body code
}
在这种情况下,除了一个全局运行环境中的命名属性在变量实例化的过程中被创建了之外,函数对象并没有被创建,而是一个指向它的引用被赋值给了全局对象的命名属性(译者:应该就是上文所提到的创建的命名属性),直到赋值表达式被执行了函数对象才会被创建。但是,函数对象的创建也发生在全局的运行环境下所以被创建的函数对象的[[scope]]属性所指向的作用域链仍然只包含了全局对象。
函数对象内部的函数声明和函数表达式结果在函数的运行环境中被创建这样的话它们组成了更加复杂的作用域链,思考以下代码,是谁使用内部函数声明定义一个函数并且运行了外部函数:
function exampleOuterFunction(formalParameter){
function exampleInnerFuncitonDec(){
... // inner function body
}
... // the rest of the outer function body.
}
exampleOuterFunction( 5 );
外部函数声明对应的函数对象在变量实例化的过程中在全局运行环境中被创建,因此它的[[scope]]属性包含了一项,就是只有全局对象的作用域链。
当全局代码运行到调用exampleOuterFunction的时候,一个新的为该函数调用产生的运行环境就被创建,一个活动对象/变量对象也随之产生,新运行环境的作用域形成了一个包含新活动对象的链,连在外部函数对象的[[scope]]属性指向的作用域链的前面,也就是全局变量的前面。新运行环境的变量实例化导致了内部函数定义所对应的函数对象的创建,该对象的[[scope]]属性值被赋值为它所处的那个运行环境的作用域的值。一个作用域链包含了全局对象然后是活动对象。
目前为止这是所有的自动的以及由源码结构和运行控制的作用域链的构建过程,运行环境的作用域链定义了所创建的函数对象的[[scope]]属性,[[scope]]属性定义了运行环境(以及相应的活动对象)的作用域,但是ECMAScript提供了with表达式作为修改作用域的一种方式。
—————————–Again————————————————————-
with语句计算一个表达式,如果这个表达式是一个对象那么它就会被加到当前的运行环境的作用域链头(就是在活动对象/变量对象之前)然后with语句计算另外一个表达式(可能自身是一个语句块)然后恢复运行环境的作用域链到之前的状态。
函数声明可导致变量实例化的过程中函数对象的创建,因此不会受到with语句的影响,但是函数表达式可以在with语句中执行。
/* 创建一个全局变量- y – 引用一个对象:- */
var y = {x:5}; // 具有x属性的对象文本
function exampleFuncWith(){
var z;
/* 增加一个对象使全局变量y指向作用域链头 */
with(y){
/* 计算一个函数表达式,创建一个函数对象并使局部变量z指向这个函数对象 */
z = function(){
... // inner function expression body;
}
}
...
}
/* 运行 exampleFuncWith 方法: */
exampleFuncWith();
当exampleFuncWith函数被调用,结果运行环境有一条包含全局运行环境和该函数活动对象的的作用域链。在变量实例化的过程中,with语句的运行将全局变量引用的对象添加到作用域链的首部。函数表达式运行而创建的函数对象被赋值给相应的创建该对象的作用域的运行环境的[[scope]]属性,作用域链依次包含了y指向的对象、外部函数调用运行环境的活动对象、全局对象。
当with语句及相关的块语句终止之后,运行环境就恢复原来的状态(y指向的对象被移除),但是此刻函数对象已经被创建,它的[[scope]]属性赋值了一个作用域链的引用,该链的头部是y引用的对象。