JS 闭包(closure)


这几天看到闭包一章,从工具书到各路大神博客,都各自有着不同的理解,以下我将选择性的抄(咳咳,当然还是会附上自己理解的)一些大神们对闭包的原理及其使用文章,当作是自己初步理解这一功能函数的过程吧。

首先先上链接:

简书作者波同学的JS进阶文章系列:

前端基础进阶系列

其他:

JS秘密花园

javascript深入理解js闭包

阮一峰《JavaScript标准参考教程》

一不小心就做错的JS闭包面试题

还有一些也很不错,但主要是以应用为主,原理解释没有上面几篇深入,不过作为闭包的拓展应用其实也可以看一看;

JavaScript中的匿名函数及函数的闭包


红皮书《JS高程》的闭包:

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

从这句话我们知道:闭包是一个函数

    function createComparisonFunction(propertyName) {

        return function(obj1,obj2) {
            var value1 = obj1[propertyName];
            var value2 = obj2[propertyName];
            
            if (value1 < value2) {
                return -1;
            } else if (value1 > value2) {
                return 1;
            } else {
                return 0;
            }
        };
    }

这段代码,我们能直接看出,共存在三个作用域,Global、createComparisonFunction、匿名函数funciton,因其JS的作用域链特性,后者能访问自身及前者的作用域。而返回的匿名函数即使在其他地方被调用了,但它仍可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction的作用域。我们来深入了解一下,函数执行时具体发生了什么?


当第一个函数被调用时,会创建一个执行环境(Execution Context,也叫执行上下文)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[[Scope]]。然后,使用this、arguments和其他命名参数的值来初始化函数的活动对象(Activation Object)。但在作用域链中,外部函数的活动对象处于第二位,外部函数的外部函数处于第三位,最后是全局执行环境(Global Context)。

换一个栗子:

    function createFunctions() {
        var result = new Array();
        
        for (var i=0;i<10;i++) {
            result[i] = function() {
                return i;
            };
        }
        return result;
    }
    var arr = createFunctions();
    alert(arr[0]());    // 10
    alert(arr[1]());    // 10

/这个函数返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,位置0的函数返回0,位置1的函数返回1,以此类推。但但实际上,每个函数都返回10,为什么?
数组对象内的匿名函数里的i是引用createFunctions作用域内的,当调用数组内函数的时候,createFunctions函数早已执行完毕。

这图不传也罢了,画得忒丑了。
数组内的闭包函数指向的i,存放在createFunctions函数的作用域内,确切的说,是在函数的变量对象里,for循环每次更新的i值,就是从它那儿来的。所以当调用数组函数时,循环已经完成,i也为循环后的值,都为10;

有人会问,那result[i]为什么没有变为10呢?
要知道,作用域的判定是看是否在函数内的,result[i] = function.......是在匿名函数外,那它就还是属于createFunctions的作用域内,那result[i]里的i就依然会更新

那么如何使结果变为我们想要的呢?也是通过闭包。

    function createFunctions() {
        var result = [];
        
        for (var i=0;i<10;i++) {
            !function(i) {
                result[i] = function() {console.log(i)};
            }(i);
        }
        return result;
    }
    var arr = createFunctions();
    arr[0]();
    arr[1]();
    arr[2]();

    function createFunctions() {
        var result = [];
        function fn(i) {
            result[i] = function() {console.log(i)}
        };
        for (var i=0;i<10;i++) {
            fn(i);
        }
        
        return result;
    }
    var arr = createFunctions();
    arr[0]();
    arr[1]();
    arr[2]();

    var arr = [];
    function fn(i) {
        arr[i] = function() {console.log(i)}
    }
    function createFunctions() {
        for (var i=0;i<10;i++) {
            fn(i);
        }
    }
    fn(createFunctions());
    arr[0]();
    arr[1]();
    arr[2]();

以第一种为例,通过一个立即调用函数,将外函数当前循环的i作为实参传入,并存放在立即调用函数的变量对象内,此时,这个函数立即调用函数和数组内的匿名函数就相当于一个闭包,数组的匿名函数引用了立即调用函数变量对象内的i。当createFuncions执行完毕,里面的i值已经是10了。但是由于闭包的特性,每个函数都有各自的i值对应着。对数组函数而言,相当于产生了10个闭包。

所以能看出,闭包也十分的占用内存,只要闭包不执行,那么变量对象就无法被回收,所以不是特别需要,尽量不使用闭包。


关于this对象

在闭包中使用this对象也会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的;在全局对象中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。(当然可以用call和apply)

    var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function () {
            var bibao = function () {
                return this.name;
            };
            return bibao;
        }
    };
    alert(obj.getName()());            // The Window

先创建一个全局变量name,又创建一个包含name属性的对象。这个对象包含一个方法——getName(),它返回一个匿名函数,而匿名函数又返回this.name。由于getName()返回一个函数,因此调用obj.getName()();就会立即调用它返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是”The Window”,即全局name变量的值。为什么匿名函数没有取得其波包含作用域(或外部作用域)的this对象呢?

每个函数调用时其活动对象都会自动取得两个特殊变量:thisarguments
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

    var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function () {
            var that = this;
            return function () {
                return that.name;
            };
        }
    };
    alert(obj.getName()());

thisarguments也存在同样的问题,如果想访问作用域中arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。

    var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function (arg1,arg2) {
            var arg = [];
            arg[0] = arg1;
            arg[1] = arg2;
            function bibao() {
                return arg[0]+arg[1];
            }
            return bibao;
        }
    };
    alert(obj.getName(1,2)())

obj.getName方法保存了其接收到的实参在它的变量对象上,并在执行函数结束后没有被回收,因为返回的闭包函数引用着obj.Name方法里的arg数组对象。使得外部变量成功访问到了函数内部作用域及其局部变量。

在几种特殊情况下,this引用的值可能会意外的改变。

    var name = "The Window";        
    var obj = {
        name:"My Object",
        getName:function () {
            return this.name;
        }
    };

这里的getName()只简单的返回this.name的值。

    var name = "The Window";        
    var obj = {
        name:"My Object",
        getName:function () {
            console.log(this.name);
        }
    };
    obj.getName();                        // "My Object"
    (obj.getName)();                      // "My Object"
    (obj.getName = obj.getName)();    // "The Window"

第一个obj.getName函数作为obj对象的方法调用,则自然其this引用指向obj对象。
第二个,加括号将函数定义之后,作为函数表达式执行调用,this引用指向不变。
第三个,括号内先执行了一条赋值语句,然后在调用赋值后的结果。相当于重新定义了函数,this引用的值不能维持,于是返回“The Window”


闭包与setTimeout()

setTimeout结合循环考察闭包是一个很老的面试题了


发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>