zoukankan      html  css  js  c++  java
  • Javascript闭包(Javascript Closures译文)

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

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

    Javascript闭包

    简介

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

         闭包是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)执行它呢。

    事情变得有点复杂了,因为globalVar所引用的函数对象,即exampleReturned(),它的[[scope]]链是它被创建时的执行上下文,也就是:exampleClosureForm执行时的活动对象(后面简称外部活动对象1)---->全局对象。因此,外部活动对象1现在也不能被垃圾回收,因为有外界引用指向它。
    闭包就这样形成了。之后调用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;', '', //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/

  • 相关阅读:
    Java 反射 Class类
    Java 反射 Class对象
    Java 集合 ArrayList和LinkedList的几种循环遍历方式及性能对比分析 [ 转载 ]
    Kubernetes 删除 namespace
    Rancher在Catalog中 使用Helm Chart安装应用
    asp.net core + log4net+exceptionles+DI
    Jenkins-Kubernetes-docker-自动发布
    Asp.Net Core 2.0 WebUploader FastDfs 文件上传 分段上传
    Centos7 安装并配置redis
    redis 配置文件详解
  • 原文地址:https://www.cnblogs.com/linfengtingyu1/p/3495070.html
Copyright © 2011-2022 走看看