Javascript Closures与原理

介绍

一个闭包是一个表达式(通常是函数),能在这个表达式的封闭环境中自由使用绑定在这个环境中的变量。

概括:原型链是用于对象的属性查找的,作用域链是用于函数体内标识符解析的。我们在定义函数时,其实是将该函数绑定到当前作用域的对象上,例如window对象。用"()"调用函数,实际上是对象方法的调用,所以这时被调用方法中的this关键字就是该对象。在使用new关键字创建用户自定义函数对象时,函数中的this则是,新创建的对象的引用。

闭包是ECMAScript中最强大的功能之一。如果不理解闭包,就不能被正确的利用它。闭包很容易创建,就算无意中也可能创建闭包。闭包的创建有一定的潜在威胁,尤其在常见的web浏览器环境中(例如IE中可能会内存泄露)。为了避免意外遭遇闭包而且能利用闭包提供的优点,我们需要理解它的操作原理。闭包很大程度上依赖于作用域链在标识符解析中的角色,和对象上属性命的解析。

简单的解释闭包:ECMAScript允许在其他函数体内的内部函数,函数定义和函数表达式访问所有的本地变量,参数和申明这个内部函数的外部函数。当一个使用内部函数访问包含它的外部函数时就构成了闭包,所以该内部函数可能在外部函数返回后执行。这时它的引用,仍然能够访问本地变量,参数和申明该内部函数的外部函数。外部函数返回以后,这些本地变量,参数和函数的初始值仍然有效,并且可以和内部函数交互。

不幸的是,要正确的理解闭包,需要理解它们背后的操作原理,涉及到许多的技术细节。在下文中粗略的介绍ECMA 262中一些不能跳过必须掌握的特有算法。有些人可能已经熟悉了对象命名,可以跳过下面章节。但除非你已经熟悉了闭包,能跳过所有章节可以直接去写你的闭包代码了,否则,还是建议你仔细阅读下面的内容。


对象的属性命名解析
ECMAScript只承认两类对象,本地对象和宿主对象("Native Object"&"Host Object")。其中Native Objects的子类中有一类:内置对象("Built-in Object", ECMA 262 3rd Ed Section 4.3)。Native objects属于语言本身,如 Function, Object, Boolean, Number, String, Date, Array, RegExp等。Host objects是由环境提供,例如document对象,DOM节点之类的。
Native objects是松散的,而且动态的包含命名属性的(可能部分的实现中built-in object子类并非动态的,但这无关紧要)。
一个对象的命名属性是name-value表。这个值可能引用其它对象(函数也是对象),或者原始值:String, Number, Boolean, Null 或 Undefined。Undefined是一个奇怪的原始类型。它可以赋值给一个对象的属性,但并不是移除了这个属性,这个属性仅仅是持有了一个undefined的值。
赋值
对象的命名属性可以通过赋值给一个属性名创建,或者给一个已经存在的属性名称赋值。所以:
var objectRef=new Object();//创建一个通用的JS对象
一个名为"testNumber"的属性可以如此创建:
objectRef.testNumber = 5;
/* - or:- */
objectRef["testNumber"] = 5;

对象在赋值之前没有"testNumber"属性,将会创建一个。随后任何赋值都不会创建属性,只是重新设置它们的值。
objectRef.testNumber = 8;
/* - or:- */
objectRef["testNumber"] = 8;

Javascript 对象都有原型,原型本身又可以作为对象。简单的说,原型可能也有命名属性,但这和赋值无关。如果一个对象的命名属性被赋值,而实际对象在对应的命名属性中没有该属性,那么创建这个属性并赋值给它。如果有属性,那么它的值被重置。
读取值
当从对象的属性中读取值时,原型就开始参与了。如果一个对象在属性中含有,访问者使用的属性名,那么这个属性的值被返回:
/* Assign a value to a named property. If the object does not have a
   property with the corresponding name prior to the assignment it
   will have one after it:-
*/
objectRef.testNumber = 8;
/* Read the value back from the property:- */
var val = objectRef.testNumber;
/* and  - val - now holds the value 8 that was just assigned to the
   named property of the object. */

但是所有对象都可能有原型,而原型本身也是对象,反过来说,原型也有原型。由此构成了原型链。原型链当遇到原型是null时才会停止。object构造器的默认原型是一个null原型,所以:
var objectRef = new Object(); //create a generic javascript object.

代码var objectRef = new Object();是使用内置的Object(built-in Object)的[[constructor]]方法,创建实例化对象objectRef 

new Object()或者{},创建一个对象。派生对象的内部原型隐式属性(__proto__)将指向它的创建者的原型,也就是说 objectRef.__proto__===Object.prototype所以objectRef的原型链(__proto__),仅包含一个对象Object.prototype。Object.prototype的内部原型属性[[prototype]]指向null。

用户自定义函数也是对象,通过修改该函数的prototype属性,从而修改函数派生对象的内部[[prototype]]属性。


function MyObject1(formalParameter){
    this.testNumber = formalParameter;
}
function MyObject2(formalParameter){
    this.testString = formalParameter;
}
MyObject2.prototype = new MyObject1( 8 );
var objectRef = new MyObject2( "String_Value" );

变量objectRef引用MyObject2的实例,有一个原型链。在原型链中的第一个对象是MyObject1创建的实例(例如内存中的命名是"varMyobject1")。 varMyobject1也有原型对象,原型对象的[[prototype]]指向built-in Object的原型属性Object.prototype。Object.prototype的原型[[prototype]]指向null的,此刻原型链就结束了。objectRef内部原型[[prototype]]的指向关系:objectRef.[[prototype]] --> varMyobject1; varMyobject1.[[prototype]] --> MyObject1.prototype;MyObject1.prototype.[[prototype]] --> Object.prototype; Object.prototype.[[prototype]] --> null。

当一个属性访问者,尝试从一个对象的引用变量objectRef读取一个属性名,整条原型链就会进入处理过程。简单的场景:
var val = objectRef.testString;
objectRef引用MyObject2的实例,有一个名为"testString"的属性,它的值是"String_Value",所以这个值也就被赋值给了变量val,但是:
var val = objectRef.testNumber;
在MyObject2的实例本身读取不到这个属性。但变量 val却被赋值为8了,而不是undefined。虽然在当前对象自身的命名属性中找不到,解析器会轮询对象的原型。它的原型是MyObject1的实例varMyobject1,而这时它创建了一个属性值为8的"testNumber"的属性,所以属性访问者得出的值是8。MyObject1和MyObject2都没定义toString方法,但如果一个属性访问者,尝试从objectRef中读取toString属性的值。
var val = objectRef.toString;
这个val变量被赋值成了一个函数。这个函数是Object.prototype的toString属性(作用于对象)。所以当原型被发现缺少某个属性时,将轮次检查它的原型。它的原型Object.prototype,确实有一个toString方法,所以这个函数对象的引用被返回了。
最后:
var val = objectRef.madeUpProperty;

返回undefined,因为原型链的工作过程没有找到任何对象有一个名为"madeUpPeoperty"的属性,它最后获取到Object.prototype的原型属性null,而处理结果返回值undefined.读取属性名,返回在对象或它的原型链上找到的第一个值。给一个对象的属性赋值,如果它本身不存在这个属性,将在对象本身的属性上创建一个。

这意味着如果一个值赋值给objectRef.testNumber=3,一个"testNumber"属性将在MyObject2本身的实例上创建,而任何后续的读取该值的尝试,都将检索到这个值。属性访问者不需要再检查原型链了,但MyObject1的实例中的"testNumber"的值仍然是8,没有被改变。给objectRef的"testNumber"对象赋值,遮蔽了它原型链中的相关属性。

注意:ECMAScript定义了一个内部对象类型的内部[[prototype]]属性。这个属性是不能用script脚本直接访问的,用于解析属性访问者的对象,涉及到的内部[[prototype]]属性链,也就是对象的原型链。还存在一个公用的prototype属性,允许赋值,定义和操作内部的[[prototype]]属性。这两者之间的详细区别在ECMA 262(3rd edition)中,我们将在下章中讨论。

原型与继承


上图中的红色箭头是隐式原型[[prototype]]链。

原型仅仅用于函数的创建对象或实例时属性继承。函数本身不使用相关原型。

JS并不是一门面向对象的语言,而是面向原型的。上图的理解有多深,就说明了你对JS机制理解有多深。虽然可以通过原型来实现面向对象,但这并不是JS语言设计的初衷。

ECMAScript中所有可用于派生(new 操作)的函数,都有一个内置的[[constructor]](built-in Function constructor),一个prototype对象属性和一个[[prototype]]指针。built-in Function constructor都是同一个native code用于创建对象或实例化对象操作(例如创建执行上下之类的)。prototype属性是一个对象,它包含一个constructor属性,引用该函数本身和内部隐式[[prototype]]是指针(部分JS引擎实现中可以用JS访问对象的__proto__属性来访问内部[[prototype]]指向的对象),指向父类的prototype属性。

原型链是由内部隐式[[prototype]]构成,prototype并不参与。在一个实例化的对象中解析标识符的值时会检索它整条内部原型链[[prototype]]。Object.prototype是整个链的终结点,它的内部[[Prototype]]指向null。

内置的函数对象派生是由JS引擎实现决定。在用户定义函数派生时,会调用函数的[[constructor]],为函数创建[[scope]],而且将派生类的内部__proto__属性指向函数的prototype属性。[[scope]]的创建,请阅读下章。


执行上下文(Execution Context)简介(补充资料)

 ----”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”

执行上下文,只发生在函数调用的时候,但它的值却来自函数定义的位置。

1) JS用户定义函数在创建的时候会复制当前的执行上下文(它所定义位置的执行上下文),并赋值到函数的[[scope]]属性中备用;

2) 在调用的时候,会复制[[scope]]属性创建一个新的作用域链,然后将Activation Object放到这个作用域链的头端。

3) 在调用return后,这个新创建的作用域链被丢弃(可以垃圾回收)。

执行上下文相关的详细内容如下:

对象创建过程
JS中只有函数对象具备类的概念,因此要创建一个对象,必须使用函数对象。函数对象内部有[[Construct]]方法和[[Call]]方法,[[Construct]]用于构造对象,[[Call]]用于函数调用,只有使用new操作符时才触发[[Construct]]逻辑。
var obj=new Object(); 是使用内置的Object这个函数对象创建实例化对象obj。var obj={};和var obj=[];这种代码将由JS引擎触发Object和Array的构造过程。function fn(){}; var myObj=new fn();是使用用户定义的函数创建实例化对象。
new Fn(args)的创建过程如下(即函数对象的[[Construct]]方法处理逻辑,对象的创建过程)。另外函数对象本身的创建过程(指定义函数或者用Function创建一个函数对象等方式)虽然也使用了下面的处理逻辑,但有特殊的地方,后面再描述。
1. 创建一个build-in object对象obj并初始化
2. 如果Fn.prototype是Object类型(typeof Object),则将obj的内部[[Prototype]]设置为Fn.prototype,否则obj的[[Prototype]]设置为初始化值(即Object.prototype)
3. 将obj作为this,使用args参数调用Fn的内部[[Call]]方法
    3.1 内部[[Call]]方法创建当前执行上下文
    3.2 调用F的函数体
    3.3 销毁当前的执行上下文
    3.4 返回F函数体的返回值,如果F的函数体没有返回值则返回undefined
4. 如果[[Call]]的返回值是Object类型,则返回这个值,否则返回obj
注意步骤2中, prototype指对象显示的prototype属性,而[[Prototype]]则代表对象内部Prototype属性(隐式的)。

从上面的创建过程可以看到,函数的prototype被赋给派生对象隐式[[Prototype]]属性,这样根据Prototype规则,派生对象和函数的prototype对象之间才存在属性、方法的继承/共享关系。

用户定义的函数创建过程
JavaScript代码中定义函数,或者调用Function创建函数时,最终都会以类似这样的形式调用Function函数:var newFun=Function(funArgs, funBody);var newFun=function(){}; function newFun(){} 。创建函数的主要步骤如下:
1. 创建一个build-in object对象fn;
2. 将fn的内部[[Prototype]]指向Function.prototype;
3. 设置内部的[[Call]]属性,它是内部实现的一个方法,处理逻辑参考对象创建过程的步骤3;
4. 设置内部的[[Construct]]属性,它是内部实现的一个方法,处理逻辑参考对象创建过程;

    4.1.设置内部的[[scope]]属性,引擎会将当前执行环境的Scope Chain传给Function的[[Construct]]方法。[[Construct]]会创建一个新的Scope Chain,内容与传入的Scope Chain完全一样,并赋给fn的内部[[Scope]]属性。

5. 设置fn.length为funArgs.length,如果函数没有参数,则将fn.length设置为0;
6. 使用new Object()同样的逻辑创建一个Object对象fnProto;
7. 将fnProto.constructor设为fn;
8. 将fn.prototype设为fnProto;
9. 返回fn。
步骤1跟步骤6的区别为,步骤1只是创建内部用来实现Object对象的数据结构(build-in object structure),并完成内部必要的初始化工作,但它的[[Prototype]]、[[Call]]、[[Construct]]等属性应当为null或者内部初始化值,即我们可以理解为不指向任何对象(对[[Prototype]]这样的属性而言),或者不包含任何处理(对[[Call]]、[[Construct]]这样的方法而言)。步骤6则将按照前面描述的对象创建过程创建一个新的对象,它的[[Prototype]]等被设置了。
从上面的处理步骤可以了解,任何时候我们定义一个函数,它的prototype是一个Object实例,这样默认情况下我们创建自定义函数的实例对象时,它们的Prototype链将指向Object.prototype。
另外,Function一个特殊的地方,是它的[[Call]]和[[Construct]]处理逻辑一样。

执行上下文(Execution Context)
JavaScript代码运行的地方都存在执行上下文,它是一个概念,一种机制,用来完成JavaScript运行时作用域、生存期等方面的处理。执行上下文包括Variable Object、Variable Instatiation、Scope/Scope Chain等概念,在不同的场景/执行环境下,处理上存在一些差异,下面先对这些场景进行说明。

函数对象分为用户自定义函数对象和系统内置函数对象,对于用户自定义函数对象将按照下面描述的机制进行处理,但内置函数对象与具体实现相关,ECMA规范对它们执行上下文的处理没有要求,即它们基本不适合本节描述的内容。

执行的JavaScript代码分三种类型,后面会对这三种类型处理上不同的地方进行说明:
1. Global Code,即全局的、不在任何函数里面的代码,例如一个js文件、嵌入在HTML页面中的js代码等。
2. Eval Code,即使用eval()函数动态执行的JS代码。
3. Function Code,即用户自定义函数中的函数体JS代码。

基本原理
在用户自定义函数中,可以传入参数、在函数中定义局部变量,函数体代码可以使用这些入参、局部变量。背后的机制是什么样呢?
当JS执行流进入函数时,JavaScript引擎在内部创建一个对象,叫做Variable Object。对应函数的每一个参数,在Variable Object上添加一个属性,属性的名字、值与参数的名字、值相同。函数中每声明一个变量,也会在Variable Object上添加一个属性,名字就是变量名,因此为变量赋值就是给Variable Object对应的属性赋值。在函数中访问参数或者局部变量时,就是在variable Object上搜索相应的属性,返回其值。
一般情况下Variable Object是一个内部对象,JS代码中无法直接访问。规范中对其实现方式也不做要求,因此它可能只是引擎内部的一种数据结构。

作用域或作用域链的大致概念:JavaScript引擎将不同执行位置上的Variable Object按照规则构建一个链表,在访问一个变量时,先在链表的第一个Variable Object上查找,如果没有找到则继续在第二个Variable Object上查找,直到搜索结束。

下面是各个方面详细的处理。

Global Object
Global Object是一个宿主对象。JavaScript的运行环境都必须存在一个唯一的全局对象-Global Object,例如HTML中的window对象。Global Object除了作为JavaScript运行时的全局容器应具备的职责外,ECMA规范对它没有额外要求。它包Math、String、Date、parseInt等JavaScript中内置的全局对象、函数(都作为Global Object的属性),还可以包含其它宿主环境需要的一些属性。

Variable Object
上面简述了Variable Object的基本概念。创建Variable Object,将参数、局部变量设置为Variable Object属性的处理过程叫做变量实例化(Variable Instatiation),后面将结合Scope Chain再进行详细说明。

Global Code
Global Code中的Variable Object就是Global Object,这是Variable Object唯一特殊的地方,其它情况下JS代码无法直接访问Variable Object

var globalVariable = "WWW";
document.write(window.globalVariable);  // result: WWW
上面代码在Global Code方式下运行,根据对Variable Object的处理,定义变量globalVariable时就会在Global Object(即window)对象上添加这个属性,所以输出是WWW这个值。

Function Code
Function Code中的Variable Object也叫做Activation Object(Variable Object主要和本地变量实例化相关,Activation Object主要和参数相关。但实际上它们是同一个对象。Global和eval code中不存在参数,所以也就没有Activation Object)。
每次进入函数执行都会创建一个新的Activation Object对象,然后创建一个arguments对象并设置为Activation Object的属性,再进行Variable Instantiation处理。
在退出函数时,Activation Object会被丢弃(可以被垃圾回收了)。

附arguments对象的属性:
length: 为实际传入参数的个数。注意,参考函数对象创建过程,函数对象上的length为函数定义时要求的参数个数;
callee: 为执行的函数对象本身。目的是使函数对象能够引用自己,例如需要递归调用的地方。
function fnName(...) { ... }这样定义函数,它的递归调用可以在函数体内使用fnName完成。var fn=function(...) { ... }这样定义匿名函数,在函数体内无法使用名字引用自己,通过arguments.callee就可以引用自己而实现递归调用。
参数列表: 调用者实际传入的参数列表。这个参数列表提供一个使用索引访问实际参数的方法。Variable Instantiation处理时会在Activation Object对象上添加属性,前提是函数声明时有指定参数列表。如果函数声明中不给出参数列表,或者实际调用参数个数与声明时的不一样,可以通过arguments访问各个参数。

Eval Code
Eval Code中的Variable Object就是调用eval时当前执行上下文中的Variable Object。在Global Code中调用eval函数,它的Variable Object就是Global Object;在函数中调用eval,它的Variable Object就是函数的Activation Object。
// Passed in FF2.0, IE7, Opera9.25, Safari3.0.4
function fn(arg){
     var innerVar = "variable in function";
    eval(' \
        var evalVar = "variable in eval"; \
        document.write(arg + "<br />"); \
        document.write(innerVar + "<br />"); \
    ');
    document.write(evalVar);
}
fn("arguments for function");
输出结果是:
arguments for function
variable in function
variable in eval

说明: eval调用中可以访问函数fn的参数、局部变量;在eval中定义的局部变量在函数fn中也可以访问,因为它们的Varible Object是同一个对象。

Scope/Scope Chain
首先Scope Chain是一个类似链表/堆栈的结构,里面每个元素基本都是Variable/Activation Object。
其次存在执行上下文的地方都有当前Scope Chain,可以理解为Scope Chain就是执行上下文的具体表现形式。

Global Code
Scope Chain只包含一个对象,即Global Object。在开始JavaScript代码的执行之前,JS引擎会创建好这个Scope Chain结构。

Function Code
函数对象在内部都有一个[[Scope]]属性,用来记录该函数所处位置的Scope Chain。
定义函数时,引擎会将当前执行环境的Scope Chain传给Function的[[Construct]]方法。[[Construct]]会创建一个新的Scope Chain,内容与传入的Scope Chain完全一样,并赋给被创建函数的内部[[Scope]]属性。
调用函数时,也会创建一个新的Scope Chain,包括同一个函数的递归调用,退出函数时这个Scope Chain被丢弃。新建的Scope Chain第一个对象是Activation Object,接下来的内容与内部[[Scope]]上存储的Scope Chain内容完全一样。

Eval Code
进入Eval Code执行时会创建一个新的Scope Chain,内容与当前执行上下文的Scope Chain完全一样。

实例说明

以上就是Scope Chain的原理。要理解这些是如何运作起来的,必须结合JS代码的执行、Variable Instantiation的具体细节处理。

下面用一个简单的场景来综合说明。假设下面是一段JavaScript的Global Code:

var outerVar1="variable in global code";
function fn1(arg1, arg2){
     var innerVar1="variable in function code";
     function fn2() {  return outerVar1+" - "+innerVar1+" - "+" - "+(arg1 + arg2); }
     return fn2();
}
var outerVar2=fn1(10, 20);
执行处理过程大致如下:
1. 初始化Global Object即window对象,Variable Object为window对象本身。创建Scope Chain对象,假设为scope_1,其中只包含window对象。
2. 扫描本JS段内的JS源代码(读入源代码、可能有词法语法分析过程),从结果中可以得到定义的变量名、函数对象。按照扫描顺序: 
     2.1 发现变量outerVar1,在window对象上添加outerVar1属性,值为undefined;
     2.2 发现函数fn1的定义,使用这个定义创建函数对象,传给创建过程的Scope Chain为scope_1。将结果添加到window的属性中,名字为fn1,值为返回的函数对象。注意fn1的内部[[Scope]]就是scope_1。另外注意,创建过程并不会对函数体中的JS代码做特殊处理,可以理解为只是将函数体JS代码的扫描结果保存在函数对象的内部属性上,在函数执行时再做进一步处理。这对理解Function Code,尤其是嵌套函数定义中的Variable Instantiation很关键;
     2.3 发现变量outerVar2,在window对象上添加outerVar2属性,值为undefined;
3. 执行outerVar1赋值语句,赋值为"variable in global code"。
4. 执行函数fn1,得到返回值:
     4.1 创建Activation Object,假设为activation_1;创建一个新的Scope Chain,假设为scope_2,scope_2中第一个对象为activation_1,第二个对象为window对象(取自fn1的[[Scope]],即scope_1中的内容);
     4.2 处理参数列表。在activation_1上设置属性arg1、arg2,值分别为10、20。创建arguments对象并进行设置,将arguments设置为activation_1的属性;
     4.3 对fn1的函数体执行类似步骤2的处理过程:
         4.3.1 发现变量innerVar1,在activation_1对象上添加innerVar1属性,值为undefine;
         4.3.2 发现函数fn2的定义,使用这个定义创建函数对象,传给创建过程的Scope Chain为scope_2(函数fn1的Scope Chain为当前执行上下文的内容)。将结果添加到activation_1的属性中,名字为fn2,值为返回的函数对象。注意fn2的内部[[Scope]]就是scope_2;
     4.4 执行innerVar1赋值语句,赋值为"variable in function code"。
     4.5 执行fn2:
         4.5.1 创建Activation Object,假设为activation_2;创建一个新的Scope Chain,假设为scope_3,scope_3中第一个对象为activation_2,接下来的对象依次为activation_1、window对象(取自fn2的[[Scope]],即scope_2);
         4.5.2 处理参数列表。因为fn2没有参数,所以只用创建arguments对象并设置为activation_2的属性。
         4.5.3 对fn2的函数体执行类似步骤2的处理过程,没有发现变量定义和函数声明。
         4.5.4 执行函数体。对任何一个变量引用,从scope_3上进行搜索,这个示例中,outerVar1将在window上找到;innerVar1、arg1、arg2将在activation_1上找到。
         4.5.5 丢弃scope_3、activation_2(指它们可以被垃圾回收了)。
         4.5.6 返回fn2的返回值。
   4.6 丢弃activation_1、scope_2。
   4.7 返回结果。
5. 将结果赋值给outerVar2。

其它情况下Scope Chain、Variable Instantiation处理类似上面的过程进行分析就行了。

根据上面的实例说明,就可以解释下面这个测试代码的结果:
// Passed in FF2.0, IE7, Opera9.25, Safari3.0.4
function fn(obj){
     return {
         // test whether exists a local variable "outerVar" on obj
        exists: Object.prototype.hasOwnProperty.call(obj, "outerVar"),
         // test the value of the variable "outerVar"
        value: obj.outerVar
    };
}
var result1 = fn(window);
var outerVar = "WWW"; 
var result2 = fn(window);

document.write(result1.exists + " " + result1.value);  // result: true undefined
document.write("<br />");
document.write(result2.exists + " " + result2.value);  // result: true WWW
result1调用的地方,outerVar声明和赋值的语句还没有被执行,但是测试结果window对象已经拥有一个本地属性outerVar,其值为undefined。result2的地方outerVar已经赋值,所以window.outerVar的值已经有了。实际使用中不要出现这种先使用,后定义的情况,否则某些情况下会有问题,因为会涉及到一些规范中没有提及,不同厂商实现方式上不一致的地方。

一些特殊处理
1. with(obj) { ... }这个语法的实现方式,是在当前的Scope Chain最前面位置插入obj这个对象,这样就会先在obj上搜索是否有相应名字的属性存在。其它类似的还有catch语句。
2. 前面对arguments对象的详细说明中,提到了对函数递归调用的支持问题,了解到了匿名函数使用arguments.callee来实现引用自己,而命名函数可以在函数体内引用自己,根据上面Scope Chain的工作原理我们还无法解释这个现象,因为这里有个特殊处理。
任何时候创建一个命名函数对象时,JavaScript引擎会在当前执行上下文Scope Chain的最前面插入一个对象,这个对象使用new Object()方式创建,并将这个Scope Chain传给Function的构造函数[[Construct]],最终创建出来的函数对象内部[[Scope]]上将包含这个object对象。创建过程返回之后,JavaScript引擎在object上添加一个属性,名字为函数名,值为返回的函数对象,然后从当前执行上下文的Scope Chain中移除它。这样函数对象的Scope Chain中第一个对象就是对自己的引用,而移除操作则确保了对函数对象创建处Scope Chain的恢复。

this关键字处理
执行上下文包含的另一个概念是this关键字。
Global Code中this关键字为Global Object即window对象;函数调用时this关键字为调用者,例如obj1.fn1(),在fn1中this对象为obj1;Eval Code中this关键字为当前执行上下文的Variable Object。

在函数调用时,JavaScript提供一个让用户自己指定this关键字值的机会,即每个函数都有的call、apply方法。例如:
fn1.call(obj1, arg1, arg2, ...)或者fn1.apply(obj1, argArray),都是将obj1作为this关键字,调用执行fn1函数,后面的参数都作为函数fn1的参数。如果obj1为null或undefined,则Global Object将作为this关键字的值;如果obj1不是Object类型,则转化为Object类型。它们之间的唯一区别在于,apply允许以数组的方式提供各个参数,而call方法必须一个一个参数的给。

前面的测试示例代码中有多处运用到了这个方法。例如window对象并没有hasOwnProperty方法,使用Object.prototype.hasOwnProperty.call(window, "propertyName")也可以测试它是否拥有某个本地属性。

再来解释一种特殊情况:

function fn(){
     var innerVar = "variable in function";
    testVar="strange";
}
fn();
alert(testVar);//alert(window.testVar)
输出结果是:

strange

因为testVar没有使用var关键字申明,所以调用fn函数时并不会将testVar当作本地变量放入到Activation Object中,在执行到testVar="strange",会从作用域链上查找该对象。最终在停留在作用域链的末端,也就是window对象上。所以在函数执行后,window对象上会添加一个testVar属性,值为"strange"。


标识符解析,执行上下文和作用范围链(原文翻译)

执行上下文:

执行上下文是ECMSScript(ECMA 262 3rd edition)白皮书中使用的一个抽象概念,定义JS引擎必须实现的行为。白皮书中关于执行上下文应该如何实现没有任何说明,但白皮书定义了执行上下文相关的属性结构,所以它们可能被认为(甚至实现为)是对象的属性,虽然不是公共属性。执行上下文是用来完成JavaScript运行时作用域、生存期等方面的处理。执行上下文包括Variable Object、Variable Instatiation、Scope/Scope Chain等概念。

函数对象分为用户自定义函数对象和系统内置函数对象。

所有的JS代码在一个执行上下文中执行。全局代码(行内执行代码(嵌入到html元素上的JS代码),普通的JS文件,或HTML页面,loads)在全局执行上下文中执行,而每个函数的调用(也许作为一个构造器)有一个相关的执行上下文。eval函数的代码执行获取了一个独特的上下文,JS程序员平时很少使用eval,这里就先不讨论了。执行上下文的细节在ECMA 262(3rd edition)的10.2章节中介绍。

当JS函数被调用时,将进入它的执行上下文,如果别的函数被调用(或者同一个函数递归),在函数调用的持续时间内将创建并执行进入一个新的执行上下文。当调用的函数return时,返回到原先的执行上下文。如此JS代码组成了一个执行上下文的栈堆。

当一个执行上下文被创建时,一系列定义好的事情会发生:

1) 在一个函数的执行上下文中,创建一个"Activation"对象。activation对象是白皮书的另一个机制。可以认为它是一个对象,它能访问的属性名。但它不是一个普通的对象,因为它没有prototype(至少没有定义prototype),而且不能直接被javascript代码访问。

2) 为函数调用创建一个执行上下文的arguments对象(按函数调用传入的参数顺序,有整数索引类似于数组的对象)。它也有length和callee属性(具体区别本文中将不做详细解释)。Activation对象上创建一个"arguments"属性,并且将arguments对象赋值给该属性。

3) 一个作用域赋值给了执行上下文,作用域由对象列表(或对象链)组成。每个函数对象都有一个内部[[scope]]属性(函数定义时创建,稍后我们将深入这个内容),也是由对象列表(或对象链)组成。作用域被JS引擎赋值给函数调用的上下文,是由列表构成,列表指向相关函数对象的[[scope]]属性,将Activation对象加入到链的最前端(或者列表的最高处)。

4) 变量实例化("variable instantiation")的过程,产生一个Variable对象。Variable对象为每个函数的形式参数创建命名属性,而且如果函数调用这些参数有值,那么这些参数值将被赋值给属性(否则赋值undefined)。内部函数定义用于创建函数对象,用函数定义中相关的函数名称,赋值给Variable对象的命名属性。变量实例化的最后一步,用所有在函数里申明的相关本地变量,创建Variable对象的命名属性。

在变量初始化时,只是先在Variable对象上创建的属性,相关的本地变量立即赋值为undefined。事实上本地变量并没有实例化,直到在函数代码体内的相关的赋值表达式,被JS执行流执行。

事实上Activation对象的和它的arguments属性,Variable对象和函数本地变量相关的命名属性,是同一个对象。这允许arguments标识符当作函数的本地变量来看。

5) 给关键字this赋值。如果该值指向一个对象,那么this关键字的属性访问前缀,指向这个对象的属性。如果该值(内部的)是null,那么this关键字将指向global对象。

全局执行上下文有一些轻微不同的处理,因为它没有arguments,所以它不需要定义Activation对象指向它们。全局执行上下文也需要一个作用域,而它的正好是一个由global对象构成的作用域链。全局执行上下文也检查变量实例。它的内部函数(由大量JS代码构成的)通常是顶级函数申明。global对象被当作Variable对象使用的,这也是为什么全局申明函数变成了global的对象。这也适用于全局申明变量。

全局执行上下文中的this对象指向global对象。


作用域链和[[scope]]

函数调用时执行上下文的作用域链,是通过添加Activation/Variable对象到作用域链的头端(函数对象[[scope]]属性持有的)。所以理解内部[[scope]]属性如何定义很重要。

ECMAScript中的函数是对象,通过函数申明,函数表达式计算时或通过调用Function构造器创建。

用Function构造器创建的函数对象,[[scope]]属性总是指向一个仅包含global对象的作用域链。

函数申明或函数表达式创建的函数对象,将创建它们的地方的执行上下文的作用域链,赋值给它们的内部[[scope]]属性。


最简单的全局函数申明,例如:
function exampleFunction(formalParameter){
    ...   // function body code
}

在全局执行上下文的variable instantiation时函数对象被创建。全局执行上下文有一个仅由global对象构成的作用域链。

因此被创建的函数对象通过global对象的属性"exampleFunction"引用,并且内部[[scope]]属性被赋值仅含有global对象的作用域链。

 另一实现方式:
var exampleFuncRef = function(){
    ...   // function body code
}

这种情况下,在全局执行上下文变量实例化中global对象的命名属性exampleFuncRef被创建。但函数对象还没有创建。而且在赋值表达式执行前,它的值是undefined。但这个函数的创建仍然发生在全局执行上下文中,所以函数的对象内部[[scope]]属性也是仅包含global对象的作用域链。


一个函数对象内的内部函数,申明和表达式结果,在一个函数的执行上下文中创建的。所以它们得到更详尽的作用域链。参考下面的代码,用内部函数exampleInnerFunctionDec申明了一个函数,然后执行外部函数exampleOuterFunction(5)。
function exampleOuterFunction(formalParameter){
    function exampleInnerFuncitonDec(){
        ... // inner function body
    }
    ...  // the rest of the outer function body.
}
exampleOuterFunction( 5 );

外部函数定义对应的函数对象,是在全局执行上下文变量实例化时创建。所以它的[[scope]]属性,包含一个仅含有global对象的作用域链。

当全局代码执行exampleOuterFunction的调用,一个新的执行上下文被创建于函数调用,而且带着一个Activation/Variable对象。新的执行上下文的作用域变成了由新的Activation对象构成的链路,跟着由外部函数对象的[[scope]]属性指向的链(就是global对象)。新的执行上下文结果变量实例化,导致与内部函数定义相关的函数对象的创建,而且该函数对象的[[scope]]属性被来自创建它的执行上下文作用域的赋值。一个含有Activation对象的作用域链,跟着global对象。

到此为止,所有这些源代码构造和执行都是JS引擎自动控制的。执行上下文的作用域链决定了,它所创建函数对象的[[scope]]属性,而函数对象的[[scope]]属性,定义了一个用于它们执行上下文的作用域(和相应的Activation对象)。但ECMAScript提供了一个with语法,修改作用域链。

with语法执行表达式,并且如果表达式是一个对象,那么它将被加入到当前执行上下文的作用域链, Activation/Variable对象的头端。然后执行with语法的其他语句(可能是一个代码块),然后恢复它之前的执行上下文。
申明函数不能受with语句影响,因为他们在变量实例化时会导致函数对象创建。但函数表达式能在with语句里执行。
var y = {x:5};
function exampleFuncWith(){
    var z;
    with(y){
        z = function(){
        }
    }
}
exampleFuncWith();

exampleFuncWith函数被调用时,执行上下文有一个作用域链,由它的Activation对象跟着global对象构成。在函数表达式的执行时,with语句的执行,将添加指向全局变量的对象 y,到作用域链头端,产生新的作用域链scopeA。with(y)中创建的函数对象,[[scope]]属性被赋值scopeA。一个作用域链构成:y对象  -->  来自外部函数调用的执行上下文的Activation对象  -->  global对象。

当with语句相关的块语句结束,执行上下文的作用域被恢复(y对象被移除)。但是,那时创建的函数对象和它的[[scope]]属性(用头端的对象y的作用域链),却被成功赋值给了z


标识符解析
JS引擎根据作用域链解析标识符。在ECMA 262中将this归类为关键字,而不是标识符。这样设计不无道理,因在执行上下文中使用解析标识符,常常依赖于this的值,而不是引用作用域链。

标识符解析从作用域链中的第一个对象开始。检查它是否含有一个与标识符相关的属性名。因为作用域链是一串对象,这个检查包含它的原型链(如果它有的话)。如果在作用域链的第一个对象上没有找到相关的值,将会搜索下一个对象。如此类推,直到在链路中的一个对象(或它的原型)有关于标识符的这个属性名,或者作用域链到头了。

在标识符上的操作和上面描述的在对象上属性访问者的使用是一样的。对象标识符在作用域链中,有相关属性代替在属性访问者中的对象,而且标识符扮演着对象的属性名。global对象永远在作用域链的末端。

作为函数调用相关的执行上下文,将含有Activation/Variable对象在链路的头端。在函数体内使用的标识符是有效的,首先检查它们是否与形式参数,内部函数申明名称或本地变量相关。这些将被Activation/Variable对象的命名属性解析。 


Closures
自动垃圾回收

ECMAScript采用自动垃圾回收。白皮书没有定义细节,留给JS引擎的实现去解决,而且一些已知的实现给他们的垃圾回收操作很低的优先级。但JS引擎中普遍认为,一个对象如果没有引用时(剩余的执行代码中没有访问了),垃圾回收就能被垃圾收回了,并且将一些指向的指针销毁,然后释放它的资源,最后返回给系统重用。

这通常,在退出执行上下文时。作用域链结构,Activation/Variable对象和在执行上下文中创建的任何对象,包括函数对象,如果不再被访问,都将被垃圾回收释放。


形成闭包
闭包的形成是通过:返回一个内部函数对象,该内部函数对象在外部函数调用时产生的执行上下文内创建的,而且将该内部函数的引用,赋值给其他对象的属性。或者,通过直接指定这些函数对象的引用给一个对象,例如全局变量,全局访问对象的属性,或者一个通过外部函数调用传入作为参数的对象。
function exampleClosureForm(arg1, arg2){
    var localVar = 8;
    function exampleReturned(innerArg){
        return ((arg1 + arg2)/(innerArg + localVar));
    }
    return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);

现在exampleClosureForm调用的执行上下文中所创建的函数对象,不能被垃圾回收。因为它指向一个全局变量 globalVar ,而且仍然能访问,甚至还能被globalVar(n)执行。

其实在此发生了更复杂的事情。因为现在通过globalVar指向的函数对象,由创建它时的执行上下文(全局执行上下文)内部的[[scope]]属性创建,一个包含Activation/Variable对象的作用域。现在Activation/Variable对象也不能被垃圾回收,因为被globalVar引用的函数对象的执行,需要将来自它的[[scope]]属性的整个作用域链,加入到每次调用它globalVar()所创建的执行上下文的作用域。

一个闭包组成了。内部函数对象exampleReturned有自由变量,而且在函数的作用域链上的Activation/Variable对象上绑定这些环境变量。

通过把作用域链赋值给globalVar引用的函数对象内部[[scope]]属性的持续引用,这个Activation/Variable对象被困住了。Activation/Variable对象被保藏其状态:它的属性值。调用的执行上下文中内部函数的作用域解析,将Activation/Variable对象相关的命名属性作为该对象的属性,解析标识符。这些属性的值,能仍然被读取或者设置,即使创建它的执行上下文退出了。

在上面的例子中,在外部函数返回时(即退出它的执行上下文时),Activation/Variable对象有一个状态,代表形式参数的值,内部函数定义和本地变量。arg1属性的值是2,arg2属性的值是4,localVar的值是8,一个exampleReturned的属性,指向被返回给外部函数的内部函数对象。(我们将这个Activation/Variable对象的引用作为"ActOuter1"便于稍后讨论)。

如果exampleClosureForm函数被再次调用:
var secondGlobalVar = exampleClosureForm(12, 3);

新的执行上下文连同新的Activation对象被创建。而且新的函数对象被返回给secondGlobalVar,使用它独有的[[scope]]属性,指向来自第二个执行上下文的,一个包含Acitvetion对象的作用域链。arg1是12,而arg2是3.(我们将这个Activation/Variable对象的引用作为"ActOuter2"便于稍后讨论)。

通过第二次执行exampleClosureForm,第二个单独的闭包形成了。

通过执行exampleClosureForm,这两个函数对象被创建而且分别赋值给全局变量globalVar和secondGlobalVar,返回表达式((arg1+arg2)/(innerArg+localVar))。它适用于这四个标识符各种操作。而解析这些标识符就是使用闭包的值。

考虑一下调用globalVar引用的函数对象的情况,如globalVar(2)。一个新的执行上下文被创建,而且一个Activation对象(我们称为"actInner1")并且被添加到作用域链头端,指向被执行函数对象的[[scope]]属性。在actInner1的形式参数后,增加了一个值为2的innerArg的属性。这个新执行上下文的作用域链是:ActInner1->ActOuter1->global object

通过作用域链完成标识符解析,为了返回表达式((arg1+arg2)/(innerArg+localVar))的值,标识符的值将通过依次查找作用域链中的每个对象上的标识符相关的名称的属性而决定。

作用域链中的第一个对象是ActInner1,它有一个值为2的nnerArg的属性。ActOuter1其它所有3个标识符相关的命名属性是:arg1是2,arg2是4,localVar是8。函数调用返回((2+4)/(2+8)).

和secondGlobalVar引用的另一个函数对象的执行比较,如secondGlobalVar(5)。为新的执行上下文"ActInner2"调用Activation对象,作用域链成了:ActInner2->ActOuter2->global object。ActInner2返回innerArg是5,而ActOuter2返回arg1是12,arg2是3,localVar是8。返回的值是((12+3)/(5+8)).

再次执行secondGlobalVar,如secondGlobalVar(3)。一个新的Activation对象(ActInner3)将出现在作用域链的头端,但ActOuter2将仍是作用域链的下一个对象而且它的命名属性的值,将再次被用于标识符的解析方式中,作用域链成了:ActInner3->ActOuter2->global object。通过作用域链得到的值是:arg1是12,arg2是3,localVar是8。

这是ECMAScript内部函数如何获取,维护和访问形式参数,申明内部函数和执行上下文的本地变量。而且它也是为何闭包的结构允许函数对象保持这些值的引用,读取和改写它们,只要它们继续存在。来自创建内部函数的执行上下文的Activation/Variable对象,通过函数对象的[[scope]]属性的引用,仍在作用域链上,直到所有相关的内部函数被释放,而且函数对象可以被回收(连同它的作用域链上所有不需要的对象)。

内部函数它们本身可能还有内部函数,而且这些内部函数返回来自函数执行的内部函数构成闭包,可能它们本身返回内部函数而且构成它们本身的闭包。在内部函数对象创建,每次嵌套,作用域链都获取到额外的Activation对象与执行上下文。ECMAScript白皮书要求一个作用域链有限制,但没有给出作用域链的长度。可能实现ECMAScript时确实指定了一些长度限制,但尚未有反馈限制的层数。嵌套内部函数的目前为止很少超过了人们想要编写它们的层数。

我们能用闭包做些什么?

答案是所有事情。闭包能使ECMAScript模仿任何事情,限制仅仅是你构思的能力和模仿的实现。这么说有些难懂,我们还是从一些实际的例子开始吧:


例子1:用函数引用setTimeout
闭包常见的用法,是函数执行之前给该函数提供参数。例如,在web浏览器中经常见到,将函数是作为setTimeout函数的第一个参数。
setTimeout延迟一段时间(它的第二个参数)后执行第一个参数提供的函数(或JS代码字符串,但不在this上下文中)。如果一段代码想使用setTimeout,它调用setTimeout函数,并且将一个函数对象作为第一个参数传入,并使设定毫秒间隔,但因延迟执行这个函数对象,引用的函数对象不能提供参数。
但代码能调用其他函数,该函数返回内部函数对象的引用。将这个内部函数对象的引用传递给setTimeout函数。用于内部函数执行的参数,通过函数调用传入并且返回它。setTimeout执行内部函数时没有传入参数,但内部函数仍然能访问这些通过外部函数调用提供的参数,返回它。
function callLater(paramA, paramB, paramC){
    return (function(){
        paramA[paramB] = paramC;
    });
}
var functRef = callLater(elStyle, "display", "none");
hideMenu=setTimeout(functRef, 500);

例子2:将函数和对象的实例方法关联
很多情形下,一个函数对象的引用被赋值,以便它能在未来某个时刻执行。给函数的执行提供参数很有用,但在执行的时候不容易,而且直到指配的时候才知道。
一个例子:JS对象可能被设计成封装特有DOM元素的交互。它有doOnCLick,doMouseOver和doMouseOut方法,而且想在DOM元素的相关事件触发时,执行这些方法。可能有许多关联不同DOM元素的js对象的实例被创建,而且单个对象实例不知道如何被实例化它们的代码雇佣。对象实例不知道如何全局引用他们本身,因为他们不知道哪个全局变量(如果有)将被赋值引用他们的实例。
如此问题成了,执行一个事件句柄函数,该函数关联一个特有的JS对象实例,而且知道调用该JS对象的哪个方法。
下面的例子使用了一个普遍的闭包函数,用元素事件句柄的实例关联对象实例。安排事件句柄调用的执行调用指定的对象实例的方法,传递事件对象,并在对象方法上引用相关元素,返回方法的返回值。
function associateObjWithEvent(obj, methodName){
    return (function(e){
        e = e||window.event;
        return obj[methodName](e, this);
    });
}
function DhtmlObject(elementId){
    var el = getElementWithId(elementId);
    if(el){
        el.onclick = associateObjWithEvent(this, "doOnClick");
        el.onmouseover = associateObjWithEvent(this, "doMouseOver");
        el.onmouseout = associateObjWithEvent(this, "doMouseOut");
    }
}
DhtmlObject.prototype.doOnClick = function(event, element){
    ... // doOnClick method body.
}
DhtmlObject.prototype.doMouseOver = function(event, element){
    ... // doMouseOver method body.
}
DhtmlObject.prototype.doMouseOut = function(event, element){
    ... // doMouseOut method body.

}

var body=new DhtmlObject("body");

任何DhtmlObject的实例能用DOM元素关联他们本身。他们不需要知道任何的关于他们雇佣者的额外代码,污染全局命名空间或用其它DhtmlObject的实例冒险。
例子3:封装函数
闭包能创建额外的作用域,这能用于组建相互联系依赖的代码,最小化意外的交互操作的风险。假如一个函数用于组建字符串,并且为了避免重复的连接操作(和大量中间字符串媒体的创建),需要使用一个数组去按顺序储存字符串部分,然后用Array.prototype.join方法输出结果(用一个空字符串作为它的参数)。这个数组将扮演输出的缓存。但这个函数本地定义的数组,函数每次执行都会导致它被重新创建。如果仅仅改变这个数组的变量内容,没有必要在函数调用时每次都重新赋值。

一种可能的方式是让这个数组成为全局变量,如此它能重用而不用重新创建。但这样做的结果将导致,数组加入全局变量然后引用给函数将使用缓存数组,而可能有第二个全局属性引用给这个数组本身,导致代码不便管理。同时,如果函数被其它地方使用,它的作者必须记得引用这个函数定义和数组定义。这也让代码不容易和其它代码整合,因为需要确定函数的名称和数组的名称在全局命名空间中的唯一性。

一个闭包允许用函数管理缓存数组(而且整洁的包装),数组依赖于函数,而且同时在全局命名空间外保持缓存数组属性名,远离命名冲突和意外交互的风险。

这里的技巧是通过在行内执行函数表达式,创建一个额外的执行上下文,而且函数表达式返回内部函数,作为外部代码的函数。缓存数组作为函数表达式的本地变量定义,在行内执行。这只发生一次,因此Array仅创建一次,但所有依赖它的函数,可以重复使用。

下面的代码创建一个函数,将返回一个HTML字符串,其中的大部分是常量,但是这些常量字符序列需要散布在作为函数调用参数提供的变量周围。

来自函数表达式的行内执行,引用给内部函数对象被返回。并且赋值给全局变量,如此它能被作为全局函数调用。缓存数组被作为本地变量定义在外部函数表达式中。它没有暴露在全局命名空间中,而且当函数调用时不需要重复创建。

var getImgInPositionedDivHtml = (function(){
    var buffAr = [
        '<div id="',
        '',   //index 1, DIV ID attribute
        '" style="position:absolute;top:',
        '',   //index 3, DIV top position
        'px;left:',
        '',   //index 5, DIV left position
        'px;width:',
        '',   //index 7, DIV width
        'px;height:',
        '',   //index 9, DIV height
        'px;overflow:hidden;\"><img src=\"',
        '',   //index 11, IMG URL
        '\" width=\"',
        '',   //index 13, IMG width
        '\" height=\"',
        '',   //index 15, IMG height
        '\" alt=\"',
        '',   //index 17, IMG alt text
        '\"><\/div>'
    ];
    return (function(url, id, width, height, top, left, altText){
        buffAr[1] = id;
        buffAr[3] = top;
        buffAr[5] = left;
        buffAr[13] = (buffAr[7] = width);
        buffAr[15] = (buffAr[9] = height);
        buffAr[11] = url;
        buffAr[17] = altText;
        return buffAr.join('');
    }); //:End of inner function expression.
})();

如果一个函数依赖于一个或几个其他函数,但是这些其他函数没被其他任何代码希望能直接调用。那么同样的技巧可被用于用一个公共暴露的函数,组建这些函数。使得复杂的多函数处理变成了简单的接口,而且封装了代码单元。


其它例子:
Probably one of the best known applications of closures is Douglas Crockford's technique for the emulation of private instance variables in ECMAScript objects. Which can be extended to all sorts of structures of scope contained nested accessibility/visibility, including the emulation of private static members for ECMAScript objects.
The possible application of closures are endless, understanding how they work is probably the best guide to realising how they can be used


意外的闭包
任何致使内部函数能访问创建它的外部函数体的JS代码,都将构造一个闭包。这使得闭包非常容易创建,而且导致:闭包作为JS语言的特性,但JS作者们并不欣赏它。导致JS作者,为了各种任务使用内部函数和采用内部函数,没有出现预料的结果,没有认识到闭包被创建了或者闭包所卷入的问题。
意外的创建闭包有很坏的效果,在IE上的内存泄露作为下一个章节描述。但他们也能影响代码执行的效果。不仅是闭包本身,确实要小心使用他们为了创建有效的代码。内部函数的使用影响效率。
一个常见的情况是内部函数用于DOM元素绑定事件。例如下面的例子,用于添加onclick句柄给一个链接元素:

var quantaty = 5;
function addGlobalQueryOnClick(linkRef){
    if(linkRef){
        linkRef.onclick = function(){
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

每当addGlobalQueryOnClick函数被调用时,一个新的内部函数被创建(通过它的赋值构成闭包)。出自效率的考虑,如果函数addGlobalQueryOnClick仅被调用一次或者两次影响并不大,但是如果函数大量的采用,大量独立的函数对象将会被创建(每次调用都产生一个新的内部函数)。
上面的代码没有使用闭包的优势。事实上,每次都是返回同样的结果。这应该申明函数完成,然后分别给事件句柄绑定,最后赋值给给函数引用。仅仅创建一个函数对象,而且所有元素使用事件句柄,共享同一个函数引用:

var quantaty = 5;
function addGlobalQueryOnClick(linkRef){
    if(linkRef){
        linkRef.onclick = forAddQueryOnClick;
    }
}
function forAddQueryOnClick(){
    this.href += ('?quantaty='+escape(quantaty));
    return true;
}

在第一个版本中,内部函数创建的闭包效率低下,不使用内部函数反而更有效率。因为不用重复创建很多个本质上完全一样的函数对象。
类似的原因也可用于构造对象函数。它很常见,看看熟悉的单例构造器:
function ExampleConst(param){
    this.method1 = function(){
        ... // method body.
    };
    this.method2 = function(){
        ... // method body.
    };
    this.method3 = function(){
        ... // method body.
    };
    /* Assign the constructor's parameter to a property of the object:-
    */
    this.publicProp = param;
}

每次使用new ExampleConst(n)构造器创建一个对象,一组新的函数对象被创建作为新对象的方法。所以更多对象实例被创建,更多函数对象跟着它们创建。

Douglas Crockford的技巧在JS对象上模拟私有成员,利用闭包结果构成赋值引用给内部函数对象,给它的构造器里的构造对象的公共属性。但如果一个对象的方法没有从闭包处获益,构造器里构成多个函数对象的创建,每个对象实例将使得实例化处理更慢,而且更多资源被消耗于容纳额外的函数对象创建。
在这种情况中,创建一次函数对象而且赋值引用给他们构造器的相关属性prototype,将更有效率。因为他们将被所有用这个构造器创建的对象共享:
function ExampleConst(param){
    this.publicProp = param;
}
ExampleConst.prototype.method1 = function(){
    ... // method body.
};
ExampleConst.prototype.method2 = function(){
    ... // method body.
};
ExampleConst.prototype.method3 = function(){
    ... // method body.
};

IE内存泄露问题
IE浏览器(4-6)在它的垃圾回收系统中有一个失误,阻止回收ECMAScript和组成环路引用的host对象。这些host对象in questiong是任意DOM节点(包括document对象和它的后代)和ActiveX对象。如果一个环路引用由一个或多个他们构成,那么这些涉及的对象将不被释放,直到浏览器关闭。而且他们消耗的内存也不能被系统使用。

一个链路引用是当两个或者更多对象相互引用彼此,某种程度上能被跟随,而且引导回起始点。例如object1有一个属性引用object2,object2有一个属性引用object3,而object3有一个属性引用会object1.用纯粹的ECMAScript对象,没有其它任何对象引用object1,object2,object3,事实上他们仅仅彼此引用,而且他们可以垃圾回收。但是在IE浏览器中,如果任何这些对象刚好有一个DOM节点或者ActiveX对象,垃圾回收看不到他们之间的环路关系被从系统的其他内容孤立而释放他们。反而他们会停留在内存中直到浏览器关闭。

闭包非常善于组成环路引用。如果一个函数对象被赋值为一个闭包,例如,DOM节点上的时间句柄,而且在它的作用域链中引用节点赋值给一个Activation/Variable对象就形成了一个环路。DOM_Node.onevent->function_object.[[scope]]->scope_chain->Activation_object.nodeRef->DOM_Node.这很容易实现,而且浏览网站的每个页面都引用一点会消耗大量的系统资源(可能全部)。

小心避免构成环路引用,而且当它们无法避免时应该有补救措施,例如使用IE的onunload事件null函数引用的事件句柄。承认这个问题,而且理解闭包(和它的原理)是在IE上避免这个问题的关键。

用Object.prototype原型中的 built-int Object constructor创建了一个对象,Object.prototype的内部原型属性[[prototype]]指向null。
// Passed in FF2.0, IE7, Opera9.25, Safari3.0.4
function fn(arg){
     var innerVar = "variable in function";
    eval(' \
        var evalVar = "variable in eval"; \
        document.write(arg + "<br />"); \
        document.write(innerVar + "<br />"); \
    ');
    document.write(evalVar);
}
fn("arguments for function");
输出结果是:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值