闭包与作用域

  • 什么是闭包?

闭包是javascript中的一个难点,学习闭包首先需要知道闭包是什么。闭包的定义是:函数对象本身和这个函数关联的作用域链的结合。单看这个闭包的定义,会觉得十分抽象,不易理解。通俗来说,闭包是能够读取其它函数内部变量的函数。

为了理解上面的定义,先来看一个闭包的简单例子:

function Fun(){
    var num = 1;
    return function(){
        console.log(num);
    };
}
Fun();

可以看到,函数Fun最后返回了一个匿名函数,且匿名函数中输出了Fun函数内定义的局部变量,这样就形成了简单的闭包。为什么返回的匿名函数可以访问到Fun函数的局部变量呢?这是因为作用域链会让子对象一级一级往上查找引用的变量,直到查找到window全局环境下。因此,子对象可以访问父对象的变量,而父对象不可以访问子对象的变量。利用这个特性,可以在函数外访问到函数内的局部变量,可以说是提供了一个非常厉害的功能。

  • 闭包与作用域链

闭包与作用域链息息相关,想要学习闭包,需要先好好了解一下作用域链哦。

作用域链上会有若干个变量对象,那么什么是变量对象呢?变量对象也叫活动对象,每一个函数都会存在着一个变量对象。变量对象就是一个函数的执行环境中的所有变量和函数,且变量对象的第一个成员就是函数的arguments变量,之后的成员就是在函数中通过var定义的局部变量和函数了。

作用域链的第一个变量对象就是执行流正执行的函数的变量对象,然后作用域链的第二个变量对象是包含第一个变量对象的函数的变量对象,以此类推,作用域链的最后一个变量对象一定是window下的变量对象(window的变量对象没有arguments成员)。

然后再通过一个简单的例子,来理解上面的结论:

function Func(){
    var member = 1;

    this.sayMem = function(){
        console.log(++member); //闭包
    };  
}
var ins = new Func();
ins.sayMem();//2
ins.sayMem();//3
var ins2 = new Func();
ins2.sayMem();//2
ins2.sayMem();//3

当执行var ins = new func()代码时,执行流进入到func函数内部,func函数的作用域链被确定:func的变量对象 ->window的变量对象。当执行ins.sayMem()代码时,执行流执行到sayMem函数内部,sayMem函数的作用域链也确定下来了:sayMem的变量对象->func的变量对象->window的变量对象。沿着作用域链往上查找,sayMem函数内部的对member变量的引用最终可以在func的变量对象中找到。

我们知道,在一般情况下,调用完一个函数后,JavaScript的垃圾回收机制会让函数内不会被引用的局部变量被回收,释放内存空间。(关于JavaScript的垃圾回收机制也是个重要的知识点,牵扯到标记清除和引用计数,这里不详细阐述了,有兴趣的小伙伴可以查阅一下相关资料~)在闭包中被引用的变量,它不会被回收,因为JavaScript识别出是闭包后,会将变量一直保存在内存中,因此之后对该变量的多次访问都是同一个变量!这也就是为什么上述例子中,ins调用两次sayMem函数后member的值会连续递增了。ins2实例因为是通过new关键字生成的,因此会被重新分配空间,闭包产生的变量自然也会新分配空间了。

从上面的阐述可以推测出,通过n个实例调用sayMem方法,内存中会消耗越来越多的空间去保存闭包的变量值。因此,当闭包的变量使用完毕时,我们最好手动释放一下闭包的变量空间,以免造成内存空间的浪费。

  • 闭包的常见错误应用function addMem(obj, i){
    obj[‘mem_’+i] = i;
    }
    function Fun(){
    var thisObj = this;
    for(var i = 5; i < 10; i++){
    setInterval(function(){addMem(thisObj, i);}, 1000);
    }
    }

    var ins = new Fun();
    console.log(ins);

理想中会觉得输出结果应该是ins添加了5个成员,分别是ins.mem_0 = 0, ins.mem_1 = 1, ins.mem_2 = 2, ins.mem_3 = 3, ins.mem_4 = 4。然而结果却是只有一个成员,就是ins.mem_5 = 5。

在调用setInterval(fun, time)函数时,很多人会认为这表示的意思是在time时间过去后立马执行fun函数,然而setInterval函数中传递的函数参数是异步的,定时器会在每一次time时间过去后,将函数添加到队列中,等到同步代码执行完毕后才会依次执行队列中的函数,且队列中不仅仅只是setInterval函数在排队,因此time只是表示在time时间过去后把fun函数加入到队列准备执行。

闭包的错误使用在于每次给addObj函数传递参数i时,由于异步代码必须等待同步代码执行完后才会执行,所以for循环结束后才开始执行队列中的addObj函数,而此时i值已经变成5了,所以无论调用多少次addObj函数,都是对同一个成员mem_5的重复赋值而已。

下面的代码是闭包的正确使用方法:

function addMem(obj, i){
    obj['mem_'+i] = i;
}
function Fun(){
    var thisObj = this;
    for(var i = 5; i < 10; i++){
        (function(curI){
            setInterval(function(){addMem(thisObj, curI);}, 1000);
        })(i);
    }
}

var ins = new Fun();
console.log(ins);

这样就会输出我们预想的添加的五个成员了。这是因为此时的闭包不是对i的闭包了,而是对curI的闭包,curI是参数,也是匿名函数的局部变量。当i++时,i的空间没变,但是匿名函数的局部变量curI已经不是以前的curI了。因为匿名函数作用域的第一个变量对象在每一次调用都会重新申请空间,因此那5次对匿名函数的调用产生的5个curI变量只是同名不同空间,且各自保存了i的值。因此会得到预想中的输出结果。

  • 小结

本文是对闭包的一些基础知识点进行了概括,网上关于闭包的资料很多,我也借鉴了一些,总结出我觉得比较重要的一些知识点。总之,闭包的作用是很大的,很多高级应用都要依靠闭包实现,关于闭包的应用还需要进一步的深入学习~

发表评论