Javascript闭包(Javascript Closures译文)

原文: http://jibbering.com/faq/notes/closures/ , 强烈推荐有能力的同窗读原文.javascript

本文不会对原文逐字逐句翻译, 但文章的结构和大致意思会与做者保持一致.文中穿插了一些我我的的理解,以蓝色字体标出.html

Javascript闭包java

简介node

    “闭包”就是一个表达式(一般是函数表达式),该表达式能够自由访问一些变量和变量所处的环境(这个环境将这些变量‘关’在了里面)。web

     闭包是JS最强大的特性之一,但在没有深入理解它以前咱们很难充分、高效地利用它。闭包一般很容易被建立,有时甚至是无心识地,但这种建立一般是有害的,尤为是在web浏览器环境中。为了不在无心识建立闭包带来的危害而充分利用它的优势,咱们须要了解闭包底层的机制。该机制主要是做用域链(scope chain)在解析标识符(identifier)及对象属性时所扮演的角色。面试

     关于闭包最简单的解释就是 ECMAScript(即Javascript)容许内部函数(inner function),内部函数是指函数声明或函数表达式是在另一个函数(称为outer function或外部函数)的函数体中定义的。内部函数能够自由访问其outer function的函数参数、局部变量及其它内部函数。当内部函数在其外部函数以外被访问时,闭包就造成了。这意味着内部函数在其外部函数返回以后依然能够被执行。当内部函数被执行时, 它所能访问的外部函数的参数、局部变量及其它inner function的值都处于外部函数返回以前的状态(至关因而保留了一份当时的现场)。编程

对象的命名属性解析数组

ECMAScript共有两种类型的对象,分别是原生对象(Native Object)和宿主对象(Host Object)其中原生对象是指js语言自己的对象(Array, Date等内置对象,以及经过js代码建立的对象如var obj = new Object();),宿主对象是指JS执行环境(客户端环境一般是浏览器,服务器端多是NodeJs等)所内置的对象,例如浏览器环境中的window对象,dom对象等。浏览器

原生对象是松散动态的命名属性的集合(Native objects are loose and dynamic bags of named properties),这些命名属性的值多是其它对象的引用,也多是一个原始值: string, numbe,boolean, null 或undefined. 其中undefined这个原始值有一点怪异,当咱们把它赋值给对象的某个属时,并不会将该属性从对象中移除,而只是该属性的值是undefined而已。缓存

下面的这个例子展现了如何给对象的属性赋值以及如何从对象中读取这个属性的值。

赋值

//建立一个普通的js对象
var objectRef = new Object(); 
//给该对象添加一个名为'testNumber'的属性:
objectRef.testNumber = 5;
/* - 或:- */
objectRef["testNumber"] = 5;
//在上面这句赋值语句执行以前,objectRef所引用的对象上并无testNumber这个属性,但在赋值以后它有了。以后再给这个属性赋值就不会再建立新属性了,而只是重置这个属性的值:
objectRef.testNumber = 8;
/* - 或:- */
objectRef["testNumber"] = 8;

每一个js对象都有一个原型(prototype),这个原型自己也是一个对象(或null,只有Object.prototype能够为null,并且这也是js语言自己的设置,任何将其它对象的原型设为null的语句都会被忽略)。该原型对象也可能有命名的属性,可是它们和赋值过程无关。赋值时,若是被赋值的对象没有对应的命名属性,则该命名属性将在该对象上被建立,并把值赋给这个新建立的属性。在此以后的赋值操做只会重置(reset)这个属性的值。

读值

在读值的过程当中,原型才真正开始发挥做用。若是对象上有对应的命名属性,则该属性的值将被返回:

 
/* 给对象的一个命名属性赋值,若是在此以前对象上没有这个属性,则该属性将被建立*/
objectRef.testNumber = 8;

/*读取这个属性的值:- */ 
var val = objectRef.testNumber; /* val的值如今是8了 */
 

因为全部的js对象都有原型,又原型也是对象,因此原型也有原型,原型的原型也有原型。。这样就造成了原型链(prototype chain).这个原型链会一直继续直到原型链中的某个对象的原型为null. Object对象的原型默认有一个null的原型(即Object.prototype.prototype = null), 所以:

var objectRef = new Object();

objectRef的原型链中将只有一个对象,即Object.prototype. 然而:

/* 
   MyObject1的构造函数
*/
function MyObject1(formalParameter){
    /* 该类的对象有一个名为‘testNumber’的命名属性  */
    this.testNumber = formalParameter;
}

/* 
   MyObject2的构造函数
*/
function MyObject2(formalParameter){
   /* 该类的对象有一个名为‘testString’ 的命名属性   */
    this.testString = formalParameter;
}

/* 下面的操做将全部MyObject2类型实例的原型由默认的对象(??Object.prototype or Function.prototype??)替换为一个MyObject1类型的对象,且该对象的testNumber属性的值为8 */
MyObject2.prototype = new MyObject1( 8 );

/* 
  最后,建立MyOject2类型的一个实例,并传递一个值给它的testString属性
*/ var objectRef = new MyObject2( "String_Value" );

如今,objectRef拥有一个原型链,该原型链上由近及远依次是MyOject2.prototype,它是一个testNumber值为8的MyObject1对象,而后是MyObject1.prototype,这是MyObject类的默认原型(这是一个仅有一个constructor属性的对象,而且该属性的值是构造函数MyObejct1()的引用),而后是这个默认原型对象的原型,即Object.prototype,因为Object.prototype的原型为null, 所以原型链终止。即objectRef的原型链为:

new MyObject1( 8 ) ---> MyOject1.prototype({constructor:MyObject1(formalParameter)}) ---> Object.prototype

当咱们试图从objectRef所引用的对象(如下简称为objectRef)中读值时,整个原型链都会进入这个搜索过程,例以下面的例子:

var val = objectRef.testString;

因为objectRef自己有一个名为testString的属性,所以该属性的值被返回("String_Value")被赋给变量val. 然而:

var val = objectRef.testNumber;

 因为objectRef中并无一个名叫testNumber的属性,所以js引擎开始检查objectRef的原型链。而后它在原型链的第一个对象(new MyObject(8))中找到了一个名为testNumber的属性,所以该属性的值被返回,查找结束。下面:

var val = objectRef.toString;

一样,objectRef中没有名为toString的属性,所以js引擎转到原型链中查找,因为原型链第一个对象中没有相应的属性,因而它接着到第二对象中去找,也没找到,因而到第三个(Object.prototype)中去找,发现Object.prototype中有这样一个属性(是一个函数),因而该属性的值被返回,查找结束。最后:

var val = objectRef.madeUpProperty;

将返回undefined,由于objectRef自己以及它的原型链上的全部对象中都没有一个名叫madeUpProperty的属性,所以查找失败,返回undefined.

从对象中读取某个命名属性的值时,将返回从对象自己或它的原型链中第一个找到的值;而赋值的过程则只跟这个对象自己有关,若它自己不存在这个命名属性,则将为它建立这样一个属性。

也就是说,若是咱们试图进行这样的赋值操做:

objectRef.testNumber = 3

将会在objectRef自己中建立这样一个属性,这以后的任何读取testNumber值的操做都将直接返回objectRef自己中这个属性的值,它的原型链将再也不被查找。可是它原型链中的这个属性的值不会被影响,依然是8. 只是读值时objectRef中的testNumber将原型链中的同名属性给遮住了,使它‘不可见’了。

 

执行上下文做用域链 和 标识符解析

执行上下文,或运行期上下文(execution context)是ECMAScript规范中的一个抽象的概念,用来定义ECMAScript实现的行为准则. 可是规范并无明肯定义执行上下文应怎样被实现,除了指出执行上下文应该包含一些关联属性,这些关联属性由规范定义。所以执行上下文能够被看成(甚至是实现)为一组拥有某些属性的对象,尽管这些属性不是public的。

全部js代码都运行在一个执行上下文中。全局代码运行在全局执行上下文中;函数(或构造函数)的每一次调用也有相应的执行上下文。经过eval()执行的代码也有相应的执行上下文,但因为eval()并不经常使用因此咱们这里不讨论它。关于执行上下文的具体细节请查阅ECMA262规范的10.2小节。

当一个js函数被调用时,js引擎进入一个执行上下文,若是在这期间另外一个函数被调用了,或是该函数递归调用了自身,则另外一个执行上下文将被建立,而后js引擎进入这个新的执行上下文,当这个函数执行完毕后,js引擎返回以前的执行上下文,从上次中断的地方继续向下执行。这样的执行机制就造成了一个执行上下文栈

当一个执行上下文被建立时,一系列的事将以固定的顺序发生:

  首先,在一个函数的执行上下文中,一个“活动对象”(Activacation Object)将被建立.这个活动对象是ECMAScript规范的又一个机制。它能够被认为是一个对象,由于它有一些可访问的命名属性。但它又不是一个常规意义上的对象,由于它没有prototype,而且它不能够被直接编程访问。

  其次,一个argument对象被建立,这是一个‘类数组’(array-like)对象,由于它的属性能够经过数字索引的方式顺序访问,而且它有一个length属性和callee属性(这点已超出咱们的讨论范围,具体请参阅ECMA262规范)。

  而后,第一步建立的活动对象被赋予一个名为arguments的属性,该属性指向第2步中被建立的arguments对象。

  接着,该执行上下文被赋予一个做用域,该做用域是由一系列对象组成的链表。每个函数对象都有一个内部的[[scope]]属性(这个咱们稍后会详细讲解),该[[scope]]属性的值也是一个对象链表。函数被调用时,其执行上下文被赋予的做用域,便是该函数的[[scope]]属性所指向的对象链表。而且,第一步中建立的活动对象将被添加到这个链表的顶部。(函数每次执行时都会建立一个新的活动对象,所以活动对象是不一样,但做用域链表的其它部分基本是相同的)

    接下来发生的是'变量实例化'(variable instantiation), 函数的全部参数、内部函数声明、局部变量和内部函数表达式(这两个是同级别的,按代码的前后顺序执行)等将依次(注意顺序,函数声明的建立在局部变量以前,所以,同名的变量将覆盖同名的函数声明,见下面的test(),这但是一道面试题哦~~)被映射成第一步所建立的活动对象的命名属性。而后,是为函数的参数赋值。若是某个参数被传递了值,则活动对象的相应命名属性将被赋予该值,不然该参数对应的命名属性的值被赋予undefined. 再而后,执行内部函数声明,活动对象上对应的命名属性的值将指向这个新建立的内部函数对象。

//这个例子是我加的,为了帮助你们理解函数的执行过程
function
test() { var a = 1; function a() {} return a; } var t = test(); console.log(typeof t); //number

须要注意的是,局部变量及内部函数表达式的值在'变量实例化'以后都是undefined,变量实例化只是在活动对象中建立了与它们一一对应的命名属性。这些命名属性只有在函数执行到对应的赋值语句时才会真正被赋值。见test2():

function test2() {
console.log(typeof b); //function
var b = 1;
console.log(typeof b); //number
    function b() {
        console.log(123);
    }
  console.log(typeof b); //number
}
test2();

在变量实例化以后, var b =1;执行以前,b被映射成活动对象上的一个同名属性b, 因为此时内部函数声明已经执行完毕,而局部变量b还没有被赋值,所以此时b的值是一个函数。而在var b = 1;执行以后,b被从新赋值, 又由于函数声明只在变量实例化时被执行一次,以后不会再被执行,由于在function b()以后和以后,b的类型都是number.  再看下面test3()和test4():

function test3() {
    console.log(typeof c);//undefined
    var c = 1;
    console.log(typeof c);//number
    var c = function() {}
    console.log(typeof c);//function
}
test3();
function test4() {
    console.log(typeof d);//undefined
    var d = function() {}
    console.log(typeof d);//function    
    var d = 1;
    console.log(typeof d);//number
}
test4();

局部变量和函数表达式的执行机制同样,所以是谁在前谁先被执行,后面的赋值将覆盖前面的。

  最后,一个叫this的关键字被赋值,若是赋予给它的值是一个对象,那么this.xx将指向该对象中相应的属性。 若是被赋的值是null(注意这个赋值操做是js引擎内部的机制,咱们是没法经过编程方式控制的),则this将指向全局对象。

  全局执行上下文有一些特殊,由于它没有arguments,因此它不须要定义一个活动对象来指向这个arguments. 可是这个全局执行上下文也有做用域,只不过这个做用域链中只有一个对象,即全局对象。 另外,全局执行上下文也会执行‘变量实例化’,在这个过程当中,全局对象自己充当了活动对象,这也是为何,在全局上下文中定义的变量和函数都是全局对象的属性的缘由了。全局上下文中,this关键字指向全局对象。

 

做用域链(scope chain)和[[scope]]

 函数调用时的做用域链,其实就是经过将活动对象添加到该函数对象的[[scope]]属性所指向的对象链表(下文简称为[[scope]]链表)的顶部造成的,所以理解内部的[[scope]]属性的定义是很重要的。

在JS中,函数也是对象,它们是经过函数声明在变量实例化期间被建立,或经过函数表达式的在代码执行期间被建立,或经过调用Function构造函数来建立。

  • 经过Function构造器建立的函数对象, 其[[scope]]链表中永远只有一个对象——全局对象。
  • 经过函数声明或函数表达式建立的函数对象, 其[[scope]]链表指向它们被建立时的执行上下文的做用域链。看下面的例子:
function foo(formalParameter){
    ...   // function body code
}

上面这个函数在全局执行上下文的‘变量实例化’期间被建立,所以它的[[scope]]链表指向全局做用域。因为全局做用域链表中只有一个全局对象,所以foo()的[[scope]]链表中也只有一个全局对象。

一个类似的例子,一样是在全局环境中,以函数表达式的方式:

var foo = function(){
    ...   // function body code
}

在这个例子中,foo()的[[scope]]链表中依然只有一个全局对象,只不过名为foo的命名属性是在变量实例化期间添加到全局对象中,而该命名属性所对应的函数对象是在执行期才被建立的。

 

因为内部函数声明和内部函数表达式所对应的函数对象是在外部函数的执行上下文中建立的,所以它们拥有更丰满的做用域链。考虑下面这个例子,在外部函数中定义了一个内部函数声明,而后执行这个外部函数:

function exampleOuterFunction(formalParameter){
    function exampleInnerFuncitonDec(){
        ... // inner function body
    }
    ...  // the rest of the outer function body.
}

exampleOuterFunction( 5 );
exampleOuterFunction()是全局上下文的’变量实例化‘过程当中被建立的,所以它的做用域链等于全局执行上下文:只有一个全局对象,exampleOuterFunction.[[scope]] = 全局对象。当全局上下文执行到exampleOuterFunction(5);这句时,一个新的执行上下文将被建立,同时被建立的还有一个活动对象。
这是exampleOuterFunction()的执行上下文,该执行上下文的做用域链由这个新建立的活动对象和exampleOuterFunction的[[scope]]链组成,即活动对象--->全局对象。而后是新执行上下文的变量实例化,在这个过程当中,一个名为exampleInnerFuncitonDec的命名属性被添加到活动对象上,该属性的值是一个函数对象。
该函数对象的[[scope]]链表被初始化为当前的执行上下文,即exampleInnerFuncitonDec.[[scope]] = ”活动对象--->全局对象“

到目前为止,一切都是js引擎自动控制的:执行上下文的做用域链定义了它内部函数对象的[[scope]]链活动对象 + 执行上下文;内部函数对象的[[scope]]链又定义了它自身的执行上下文(内部函数的做用域链将是内部活动对象+活动对象 + 执行上下文,不要将做用域链和执行上下文混淆了前者是在函数调用时存在的,包含活动对象
可是ECMAScript规范提供了with语句用来改变函数的执行上下文。with语句评估一个表达式,若是这个表达式是一个对象,那个这个对象将被添加到执行上下文(是一个对象链表)的顶部(在活动对象以前). 在with语句块中,执行上下文将被暂时改变:在链表顶部添加了一个对象。在with语句执行完毕后,该对象也将从链表顶部被删除,
执行上下文恢复成with语句以前的状态。函数声明是不会被with语句影响的,由于它们是在变量实例化期间建立的(而with语句是在代码执行期间才被评估的),而函数表达式却能够,由于它是在代码执行时才被建立的:
/* 建立一个全局变量y,指向一个对象 */
var y = {x:5}; 
function exampleFuncWith(){
    var z;
    /* 将y所指向的对象添加到做用域链的顶部 */
    with(y){
        /*经过函数表达式建立一个函数对象,并将该对象赋值给局部变量z */
        z = function(){
            ... 
        }
    }
    ... 
}

/* 执行外部函数 */
exampleFuncWith();
当exampleFuncWith();执行时,exampleFuncWith的执行上下文被建立,即活动对象--->全局对象。当执行到exampleFuncWith中的with语句时,全局变量y指向的对象被添加到执行上下文链顶部,而后z指向的函数对象被建立,该函数对象的[[scope]]链被初始化为当前的执行上下文,即:
y--->活动对象--->全局对象。当with语句结束后,y从执行上下文链中移除,但这不会改变z()的[[scope]]链,它只会记住它建立时的上下文环境。

标识符解析
标识符是沿着做用域链被解析的。ECMA262规范将this做为一个关键字而非标识符,这是有道理的,由于this的值只与它所处的执行上下文有关,而与做用域链无关。

标识符解析始于做用域链中的第一个对象,js引擎检查该对象中是否有命名属性与这个标识符相同。因为做用域链是一个对象链,所以这个检查也包括检查该对象的原型链。也就是说,检查会按这样的顺序进行:
做用域链中的第一个对象--->第一个对象的原型链--
->
第二个对象--->第二个对象的原型链--->...--->最后一个对象--->最后一个对象的原型链。直到在某个对象或它的原型链中找到对应的命名属性,则查找成功,查找终止;或一直到最后一个对象的原型链依然没有找到,则查找失败,查找终止。

对象属性的解析与标识符的解析过程一致,此时对象的属性名至关于标识符。全局对象老是处于做用域链的最末端。

因为函数被调用时,一个活动对象将被放入它的执行期上下文的做用域链的顶部,函数的全部参数、局部变量和内部函数都被映射成该活动对象的一个命名属性。所以,在函数体内,对该函数的参数、局部变量和内部函数的访问速度是最快的,它们将做为活动对象的命名属性被解析。

闭包
自动垃圾回收机制
ECMAScript 使用自动垃圾回收机制规范并没有定义该机制的细节,所以不一样的实现间可能会有一些差异,而且已知某些实现给了垃圾回收器一个很是低的优先级想法若是一个对象再也不可访问(再也不有外界引用指向它),那么将成为垃圾回收器的回收目标从此的某个时刻销毁它并释放它所占用的系统资源
一般来说是这个样子的,当js引擎退出一个执行上下文时,与该执行上下文相关的做用域链、活动对象、内部函数对象及任何其它的对象等都再也不可访问,所以也都成为垃圾回收的目标。

造成闭包
闭包的造成是经过将一个内部函数的引用赋值给一个外部变量或外部对象的属性。例如:
//闭包的例子
function
exampleClosureForm(arg1, arg2){ var localVar = 8; function exampleReturned(innerArg){
    return ((arg1 + arg2)/(innerArg + localVar));
}
/* 返回内部函数的引用 */
    return exampleReturned;
}

var globalVar = exampleClosureForm(2, 4);

 如今,经过调用exampleClosureForm()建立的函数对象(exampleReturned)将不能被垃圾回收器回收了。由于有一个全局变量引用了它,如今咱们甚至还能经过globalVar(n)执行它呢。

exampleClosureForm执行时的活动对象(后面简称外部活动对象1)---->全局对象。所以,外部活动对象1如今也不能被垃圾回收,由于有外界引用指向它。
闭包就这样造成了。建立它的执行上下文已经销毁了。
 事情变得有点复杂了,由于globalVar所引用的函数对象,即exampleReturned(),它的[[scope]]链是它被建立时的执行上下文,也就是:以后调用globalVar()时,它的执行上下文做用域链的第二个对象就是这个外部活动对象(第一个是globalVar自身相关的活动对象)。这个外部活动对象上的值仍然能够被读取和设置(见下面的例子),尽管
//设置活动对象的属性值
function
outer(){ var a = 1; function inner(b){    a += b; //a的值依然能够被设置,尽管建立它的执行上下文已经销毁了      console.log(a); } return inner; } var innerRef = outer(); innerRef(2); //打印出3,是a的新值

在上面闭包的例子中, 第一次调用exampleClosureForm时建立的外部活动对象1将保持exampleClosureForm返回时的状态,即arg1=2,arg2=4,localVar=8,exampleReturned-->(此符号意为‘指向’)func obj. 若是exampleClosureForm再次被调用,例如:

var secondGlobalVar = exampleClosureForm(12, 3);

一个新的执行上下文和新的活动对象(如下简称外部活动对象2)将被建立,同时一个新的函数对象将被返回, 该函数对象拥有它独立的[[scope]]链:外部活动对象2---->全局对象. 外部活动对象2的状态为: arg1=12, arg2=3, localVar=8, 

exampleReturned--->another func obj.

也就是说,exampleClosureForm的第二次调用,造成了一个全新的闭包。这两次调用中造成的这两个独立的函数对象,分别被 globalVar 和 secondGlobalVar 引用,如下简称globalVar() 和 secondGlobalVar()。globalVar() 和 secondGlobalVar()均返回一个表达式:((arg1 + arg2)/(innerArg + localVar)). 该表达式中的这几个标识符是如何被解析的,对理解闭包相当重要。

如今,假设咱们执行globalVar()

globalVar(2);

那么,一个新的执行上下文和一个新的活动对象将被建立(如下称为内部活动对象1),它只有一个命名属性:innerArg,值为2。该执行上下文的做用域为内部活动对象1---->外部活动对象1---->全局对象

因为标识符沿着做用域链被解析,所以,((arg1 + arg2)/(innerArg + localVar))表达式中的几个标识符将沿着上面的做用域链解析。做用域链中的第一个对象是内部活动对象1它只有一个innerArg属性,返回2,其它的几个标识符都是在外部活动对象1中找到的,分别是arg1=2,arg2=4,localVar=8, 所以globalVar(2)调用返回((2+4)/(2+8)).

接下来,执行secondGlobalVar():

secondGlobalVar(5);

另外一个活动对象(如下称为内部活动对象2),innerArg=5, 和另外一个执行上下文,做用域为:内部活动对象2---->外部活动对象2---->全局对象,被建立与上面一样的解析方式,所以secondGlobalVar(5)返回((12+3)/(5+8)).

再次执行secondGlobalVar():

secondGlobalVar(100);

新的活动对象(内部活动对象3)和新的执行上下文被建立:内部活动对象3---->外部活动对象2---->全局对象. 注意这和secondGlobalVar(5)调用的区别:做用域链顶部的活动对象是不一样的,但做用域链中第二个对象即外部活动对象2是相同的,所以不管secondGlobalVar()被调用多少次,函数的返回表达式中arg1,arg2,localVar的值永远是12,3和8.

这就是ECMAScript中内部函数在它的生存期内如何引用、访问外部函数的参数、局部变量、内部函数的机制。该内部函数被建立时的外部活动对象始终处于它的做用域链上,直到再也不有外部引用指向该内部函数,这时,该内部函数对象将成为垃圾回收器的回收目标, 同时它做用域链上全部失去外界引用的对象(包括外部活动对象)也将被回收。

内部函数自己也可能有内部函数,所以内部函数中也可能再返回函数进而造成更深层的闭包。每深一层,函数的做用域链中就多一个活动对象。ECMAScript规范要求做用域链的长度是有限的,可是没说这个限度具体是多少。不一样的JS实现(JS引擎)可能设定了不一样的长度限制但目前为止没有具体的值被公开。但这个值远远超过了你在代码中真正想要嵌套的层次。

 

闭包能作什么?

奇怪的这个问题答案彷佛任何东西一切。闭包使得JavaScript能够模拟任何东西,由于使用闭包的惟一限制是你想像的力和执确实有点深奥,所以以一些比较实际的东西开始可能比较好

例1:给setTimeout传递函数引用

使用闭包的一个常见情景就是在调用一个函数以前给它传递参数

例如,将一个函数做为参数传递给setTimeout,使setTimeout在指定时间(由第二个参数指定)以后调用它,这个js中是很常见的。可是,这样没法给该函数传递参数(固然setTimeout还有另一种用法,即第一个参数是字符串的状况,这种状况下能够将参数拼接在字符串,但这不是咱们要讨论的话题)。

为了在将函数做为第一个参数传递给setTimeout的同时,也能给该函数传递参数,咱们能够调用另外一个函数,这个函数返回内部函数的一个引用,而后将该内部函数的引用做为第一个参数传递给setTimeout. 内部函数执行时所需的参数在调用外部函数时传入。这样,setTimeout在执行该内部函数时无需传递任何参数,但该内部函数依然能够访问外界提供的参数---这些参数是在外部函数被调用时传入的:

function callLater(paramA, paramB, paramC){
    /* 返回一个匿名的内部函数的引用 */
    return (function(){
        /* 这个内部函数将经过setTimeout被执行,当它执行时,它能够读取、设置传递给外部函数的参数 */
        paramA[paramB] = paramC;
    });
}

...

/* 调用callLater,返回内部函数的一个引用给局部变量funcRef。传递给外部函数callLater的参数将在该内部函数最终被执行时使用 */
var functRef = callLater(elStyle, "display", "none");
/* 将内部函数的引用funcRef做为第一个参数传递setTimeout */ hideMenu=setTimeout(functRef, 500);

 

例2: 将函数与对象的实例方法关联

(

 There are many other circumstances when a reference to a function object is assigned so that it would be executed at some future time where it is useful to provide parameters for the execution of that function that would not be easily available at the time of execution but cannot be known until the moment of assignment.

 

One example might be a javascript object that is designed to encapsulate the interactions with a particular DOM element. It has doOnClick, doMouseOver and doMouseOut methods and wants to execute those methods when the corresponding events are triggered on the DOM element, but there may be any number of instances of the javascript object created associated with different DOM elements and the individual object instances do not know how they will be employed by the code that instantiated them. The object instances do not know how to reference themselves globally because they do not know which global variables (if any) will be assigned references to their instances.

 

So the problem is to execute an event handling function that has an association with a particular instance of the javascript object, and knows which method of that object to call.

 

The following example uses a small generalised closure based function that associates object instances with element event handlers. Arranging that the execution of the event handler calls the specified method of the object instance, passing the event object and a reference to the associated element on to the object method and returning the method's return value. 

)

这个例子大意明白,但使用闭包的精妙之处没领会。上面这段话(尤为第一段最后一句)也没领会精神,有哪位朋友看懂的,请留言告诉我一下,谢谢。

/* 为指定的dom元素绑定事件处理程序
  @param Object obj 要绑定事件处理程序的dom元素
@param String methodName 要做为事件处理程序的方法名称
@return Function 事件处理程序
*/ function associateObjWithEvent(obj, methodName){   /* 注意: 下面返回的这个方法只有在特定事件发生时,才真正被执行 */
return (function(e){ /* 兼容低版本IE和标准浏览器 */ e = e||window.event; /* 下面的this指向事件发生源的dom元素, 由于这个内部函数是做为该dom元素的事件处理程序被执行的 */ return obj[methodName](e, this); }); } function DhtmlObject(elementId){ var el = getElementWithId(elementId); if(el){ /* 绑定事件处理程序: 将DhtmlObject的指定名称的方法做为el的某个事件处理程序 */ el.onclick = associateObjWithEvent(this, "doOnClick"); //这里的this指向当前的DhtmlObject对象 el.onmouseover = associateObjWithEvent(this, "doMouseOver");//this同上
     el.onmouseout = associateObjWithEvent(this, "doMouseOut"); //this同上
     ...
  }
}
DhtmlObject.prototype.doOnClick
= function(event, element){
  ...
// doOnClick 方法体
}
DhtmlObject.prototype.doMouseOver
= function(event, element){
  ...
// doMouseOver 方法体
}
DhtmlObject.prototype.doMouseOut
= function(event, element){
  ...
// doMouseOut 方法体
}

这样一来,任何DhtmlObject对象均可以将本身与它们所感兴趣的dom元素(经过elementId)结合在一块儿了,而且不须要知道是否有其它代码与这个dom元素有关联,也不须要担忧会污染全局环境或与其它DhtmlObject对象有冲突。

 

例3: 将有关联关系的函数封装在一块儿

 闭包能够用来建立做用域以便将相互关联或相互依赖的代码组织在一块儿,同时最小化和其它代码意外交叉的风险。假设有这样一个函数,它的做用是构建一个字符串,但要避免重复的字符串链接操做以及没必要要的中间字符串的建立(例如 var a = 'aa'; var b = 'bb' + 'cc' +a; 就会创造'bbcc'这个中间字符串.). 所以咱们能够将这些字符串片断按顺序放入一个数组,最后调用Array.prototype.join(经过传递一个空字符串做为参数)输出结果字符串. 这个数组在这里充当了缓存的做用, 若是咱们在函数内部定义它, 则在函数的每次调用中都会从新建立这个数组, 而这是没必要要的,由于每次调用中这个数组只有一小部份内容会变化.

另外一个方法就是在全局上下文中定义它, 而后在函数中引用它,这样这个数组只会被建立一次. 但这样作的缺点就是不利于维护和代码复用. 假设咱们在别的工程中也要用到这个方法, 那么当咱们拷贝函数的代码时,也同时要记得拷贝函数外的这个数组的代码, 而且,在新的环境中,除了考虑函数名不能和新环境中有冲突外,还要注意数组名不能冲突.

而利用闭包就能够优雅地解决这一问题. 闭包能够优雅地将数组定义和上面这个依赖它的函数封装在一块儿,同时没必要担忧会污染全局做用域或与其它代码的冲突.

解决方案就是利用自执行的函数表达式建立一个新的执行上下文,在这个上下文中定义这个数组,并返回一个内部函数, 该内部函数的做用与上面说到的那个函数相同. 代码以下:

/*
定义一个全局变量, 它指向内部函数的引用, 所以,下面这个函数表达式返回的内部函数能够在全局做用域中执行.
这个内部函数返回一个HTML字符串, 表明了一个绝对定位的div, 包裹着一个img元素, 全部的变量都做为参数提供给函数调用
*/ var getImgInPositionedDivHtml = (function(){ /* 将buffAr定义为外部函数的局部变量, 它只会被建立一次(由于它是外部活动对象的一个命名属性,具体参见上文中'造成闭包'一节) */ 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>' ]; /* 返回一个内部函数对象, 该函数对象会在每次调用 getImgInPositionedDivHtml( ... )时被执行 */ 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 内部函数 })(); //自执行

 若是一个函数依赖于一个(或多个)其它函数, 但这些被这依赖的函数又不想被其它代码访问的话,那么能够用和上面相同的技巧来处理。即利用闭包将这些函数封装在一块儿,只暴露一个入口函数给外部调用。 这样便优雅地将一堆多函数的面向过程的代码转变成了一个封装良好的、易移植的程序单元。

其它例子

可能闭包最著名的应用之一是Douglas Crockford的 在JS对象中模拟私有实例成员。这篇文章中讲述的技巧能够扩展到各类各样的数据结构中,包括在JS对象中模拟静态成员。 (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.)

闭包能够实现的应用是无止境的,理解它的工做原理是了解如何使用它的最佳指导。

 

意外的闭包

任何使内部函数呈如今建立它的函数以外的操做都将造成一个闭包。这使得闭包很是容易被建立,js做者甚至能够在根本不了解闭包的状况下利用内部函数完成各类各样的任务,在这种状况下,因为没有明显迹象,他并不知道本身建立了闭包以及这样作将有什么影响。

意外地建立闭包可能产生有害的反作用,例如咱们下一个章节要讲的IE内存泄漏问题。此外,意外闭包还可能影响代码的性能。这并非说闭包自己会对性能产生影响,实际上,若是正确地使用,闭包能够建立至关高效的代码。真正对效率产生影响的是内部函数。

一个常见的状况就是使用内部函数做为dom元素的事件处理程序。例以下面代码将用来处理a标签上的点击事件:

/* 定义一个全局变量,它的值将被添加到a标签的href属性中 */
var quantaty = 5;
/* 为指定的连接元素添加点击事件监听, 同时将全局变量添加到它的href属性中*/
function addGlobalQueryOnClick(linkRef){ 
    if(linkRef){
        /* 将一个内部函数对象赋值为linkRef的点击事件处理程序
        */
        linkRef.onclick = function(){           
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

每一次addGlobalQueryOnClick被调用时,都将建立一个新的函数对象,而且造成一个闭包(由于linkRef.onclick能够在addGlobalQueryOnClick以外访问,而它指向addGlobalQueryOnClick中的建立的那个内部函数对象)。从性能的观点来看,若是addGlobalQueryOnClick只被调用一两次,那么这并非一个大问题; 但若是这个函数被执行N屡次,那么将会建立N多个彻底独立但却功能相同的函数对象(每一个连接元素分别对应一个独立的函数对象)。

上面的代码并无也不须要用到闭包的特性,所以生成闭包是没必要要的。一个和上面的例子效果彻底相同的但却高效的多的作法是,将做为事件处理程序的函数在addGlobalQueryOnClick以外独立定义, 而后将它的引用赋值给各个a元素的onclick属性。这样只会建立一个函数对象而后在全部的a元素之间共享它的引用:

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(){//这个赋值操做将造成一个闭包,由于this.method1能够在构造函数以外访问 ... // method body. }; this.method2 = function(){//同上,这个赋值也将造成一个闭包 ... // method body. }; this.method3 = function(){//同上,这个赋值也将造成一个闭包 ... // method body. }; /* 将参数赋值给对象的属性 */ this.publicProp = param; }

每次调用new ExampleConst(x)建立对象时,一组新的函数对象将被建立并分别赋值给对象的成员方法,这样,有越多的ExampleConst实例被建立,就有越多的函数对象被建立。

Douglas Crockford的在js对象中模拟私有成员的技巧利用了闭包的特性,而若是对象的方法并无利用到闭包的优点,那么在构造函内部给对象的方法赋值的操做将影响代码的执行效率而且会消耗更多的资源(由于会有多余的函数对象被建立).

此时,更高效的作法是只建立这些函数对象一次,而后把它们的引用分别赋值给构造函数的原型的相应属性, 这样它们就能够在构造函数所建立出的全部实例中被共享了:

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内存泄漏问题

在IE4~6中,若是某些宿主对象间存在循环引用的话,则垃圾回收器不会回收这个循环当中的任何一个对象,从而会致使内存泄漏问题(此问题在IE7中已解决)。这里的宿主对象指任意的Dom对象(包括document对象及它的全部后代)和ActiveX对象。循环链中的对象不会被回收,从而它们所占用的内存和其它系统资源也不会被释放,直到浏览器关闭。

循环引用是指两个或多个对象间的引用造成一条链,最后又指回开始的那个对象。例如对象1有一个属性指向对象2,对象2有一个属性指向对象3,而对象3又有一个属性指向对象1。 若是这个循环链中都是纯的js对象(即不包含DOM对象或ActiveX对象),那么当连接以外没有引用指向它们时,这条链是被会释放的; 但若是这个链中存在任何的DOM对象或ActiveX对象,那么IE4~6的垃圾回收器将识别不出来这是一个自引用的循环链,所以也不会释放它们,这条链中的全部对象所占的内存和资源直到浏览器关闭才能被释放.

闭包尤为容易造成循环引用。当一个函数造成闭包时,例如,被看成右值赋值给一个Dom对象的事件处理程序, 而且这个Dom对象的引用存在于该闭包函数的scope链的一个活动对象的命名属性中时,一个闭包就造成了:DOM_Node.onevent -> function_object.[[scope]] -> scope_chain -> Activation_object.nodeRef -> DOM_Node.这是很容易的发生的(见下面的例子),而且在一个大的网站中,当每一个页面中都存在不少段相似这样的代码时,系统的大部分(甚至是所有)内存将被消耗掉。

咱们应足够谨慎以免造成循环引用,当确实没法避免时,咱们能够采起必定的措施来弥补,例如在IE的unload事件中将全部事件监听程序设为null. 认识到这个问题并理解闭包和闭包的机制是避免在IE中引起这类问题的关键。

//例:由闭包造成的循环引用    
(function(){ var b=document.body; // ← 建立docement.body的引用 b.onclick=function() { // ← b.onclick 指向一个函数对象 // 这个函数对象的[[scope]]链为: 活动对象(有一个名为b的命名属性)--->全局对象. 这就造成了循环引用链,由于: document.body.onclick--->function.[[scope]]-->活动对象.b--->document.body // do something... }; })();

关于IE内存泄漏更详细的讲解和例子能够参考 http://isaacschlueter.com/2006/10/msie-memory-leaks/

相关文章
相关标签/搜索