沉睡中的土拨鼠

我爱前端

导航

闭包

  对于闭包的理解,博客园有很多了,我只从一个初学者角度来理解。

  先看段代码: 

function outer(){
        var x1=0;
        function inner(){
            return x1;
        }
        return inner;
    }
    function test(um){
        return um;
    }
    var a1=outer();
    var a2=outer();
    var arr=[];
    var arr2=[];
    var arr3=arr;
    var b1=test;
    var b2=test;

  我不用复杂拗口的专学术语来说什么是闭包,从上面看,inner就是一个闭包,它把原本私有的outer的变量和参数暴露给了outer函数以外的对象,下面的a1和a2都能访问到x1,那,当运行了a1()和a2(),这个x1的变量占据的内存消失了吗?我们做个测试:

for(var i=0;i<1000000;i++){
        outer();
    }

  这里做的是循环执行outer函数,如果x1在outer执行完就被销毁,那么上面的循环执行,是不会增加1000000个内存空间的,在火狐下执行,利用资源管理器查看火狐进程的内存消耗,执行和未执行,内存增加了2,000kb多,我不知道这个数字是不是由于outer造成的准确内存消耗,但是我们可以知道由于运行outer,内存小号增加了很多,不是一点点,我们再看下下面的测试;

for(var i=0;i<1000000;i++){
        test(1);
    }

  火狐运行前后,基本是没有什么内存改变,由上面两项测试可以看出,outer的循环执行内存增加是因为,x1占用的内存没有在outer执行完被回收,而test运行结束,1所占据的内存被回收,所以说,闭包阻止了外层函数局部变量和参数在外层函数运行结束后被销毁。

  既然闭包阻止了变量被销毁,那么,同样的代码,同样的变量名,那么,a1和a2相同吗?也就是,a1和a2的改变会影响对方吗,答案也是否定的。看第一段代码,arr和arr1都是一个空数组,但是它们不想等,因为,引用类型的变量,存放的是指针,不同变量声明一个引用数据,那么,它会告诉内存,在堆内存中开辟不同的空间,然后将这些堆内存空间的指针交给这两个变量(栈内存)中。所以比较两个引用类型的变量,虽然值看起来相同,可是它们并不相等,彼此的改变也影响不到对方。

  由此我们可以得出,闭包实际作用和引用类型数据是一样的。outer函数的返回值是一个函数(函数本身是对象),那么每次运行outer,也就相当于声明一个inner函数,实际上这个inner函数名只是其标识作用,outer返回的是一个堆内存指针,相当于,创建一个含有x1属性的对象

var a1={x1:0};
var a2={x1:0};

  上面的代码,是假象出来的,但是由此也可以明白,outer每次返回的inner函数,它所持有的x1变量是不同的,虽然名字相同,但是内存地址是不一样的,所以,a1和a2改变x1的值,对彼此都没有影响。

  ====================================================================================

  接下来,我用拗口的专业术语来解释,这一现象,我参考的是《高性能javascript》和《javascript权威指南》第五版

  要解释什么是闭包,那要从闭包产生的先决条件来说(总结与权威指南)

  1. 函数允许内嵌其它函数
  2. 函数也是数据
  3. 函数使用词法作用域

  与此同时,还有几个相关的概念要理解:内部属性[[Scope]],作用域链(Scope chain),可变对象(variable object),运行期上下文(execution context),活动对象(Activation object),标识符解析(总结于《高性能javascript》)权威指南中,活动对象称作调用对象,一个意思。

  首先,函数使用词法作用域,那么,就保证了,函数作用域在函数在定义它的作用域里运行,函数定义的时候,就将作用域链保存起来,作为自身的一个状态,上面的例子,inner在定义的时候,就保存了作用域链,当运行的时候,作为返回值给了a1和a2全局变量,那么,a1和a2就有了inner的作用域链(可能说法不准确,暂且这么山寨地理解)。

  一个函数的作用域链的产生可以看这篇文章高性能JavaScript 笔记之 第2章 数据访问,我就不多解释,直接引用里面的说法,当outer定义的时候,它的作用域链只有(全局对象),当执行var a1=outer()的时候,创建的“运行期上下文”就会为outer作用域链前面加上调用对象,这个时候“运行期上下文”的的作用域链就是(outer调用对象)+(全局对象),那么,此时,inner函数为了能够访问到outer的参数和变量,它的内部属性[[Scope]]就会引用outer的“运行期上下文”的作用域链,也就是,inner函数保存了(outer调用对象)+(全局对象)作用域链,由于outer调用对象保存了outer的变量和参数,那么,现在inner就能通过自身的作用域链来访问outer的变量,因为inner一直保存了outer调用对象,所以,调用对象不会消失,就如同全局对象不会消失一样。因此,闭包越多,就如同定义了很多全局变量一样,要等到网页被重置或则被关闭,这些变量占用的内存才会被释放。

  于此同时,运行inner函数也会在作用域链前面添加inner调用对象,那么,在查找x1时候,就要花更多的时间,这一点也反映,不要滥用闭包。

  其次,由于运行期上下文(execution context)是唯一的,每次运行,都会创建新的运行期上下文(execution context),所以,inner对象每次被返回,都会拥有不同的堆内存地址,所以,var a1=outer();var a2=outer();两个变量保存了不同的指针,这些指针指向的堆内存空间,里面拥有一样的代码,但是存放在不同的空间,所以,a1和a2互不干扰。

  ========================================================================================

  本人的山寨解释:

  闭包,就如同在全局运行环境中,创建一个类似全局的运行环境,里面的变量,也不会消失,如同全局变量。哈哈,太山寨了,娱乐娱乐。

posted on 2012-05-20 11:00  hoowall  阅读(113)  评论(0)    收藏  举报