前端开发JS

通俗的解释 - 什么叫JS的“闭包”

2016-08-22  本文已影响794人  microkof

闭包的英文是Closure。无论从中文还是英文看,你都猜不出它具体是啥意思。

词法作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。

JS变量跟其他语言比起来简单得多,无非就是两种:全局变量和局部变量。局部变量就是函数内部变量。

JS的变量作用域也比其他的语言简单得多。什么叫变量作用域?从字面就很好理解:变量能起作用的领域。对应变量的分类,作用域也分成两种:全局作用域、函数作用域。函数作用域是在函数定义的时候确定的,而不是在函数执行的时候确定的,所以这种作用域往深了说叫词法作用域。为什么叫“词法”,简单理解就是“按写的代码为法律”。如果什么书上说到词法作用域,就记得这是说JS的作用域就行了。跟词法作用域相对的是动态作用域,在JS里面没有这东西。

JS语言的特殊之处,就在于:

基础知识

直接看下面带闭包的代码:

function foo() {
    var a = 1;
    function bar() {
        a = a + 1;
        console.log(a);
    }
    return bar;
}

var closure = foo();
// 上面这时候返回的是 bar() 这个函数外加其包上的变量 a,也就是说,其实是:
// var closure = function bar() {
//        a = a + 1;
//        console.log(a);
// };以及,bar()对外层作用域的引用;
var closure2 = foo(); // 这里同样生成了另外一个闭包(实例)
closure(); // 2,closure虽然定义在外层函数之外,但是它却指向了内层函数,下同
closure2(); // 2 , 绑定了另外一份变量 a
closure(); // 3,closure引用的变量a并没有销毁,还能继续+1。下同。
closure2(); // 3

然后我们再对比一段不带闭包的代码:

function foo() {
    function bar() {
        var a = 1;
        a = a + 1;
        console.log(a);
    }
    return bar;
}

var closure = foo(); // 这个时候返回的是 bar() 这个函数,外层作用域并没有变量a
var closure2 = foo();
closure(); // 2,变量a在函数执行一次之后就被销毁了
closure2(); // 2
closure(); // 2
closure2(); // 2

显然,第一段带闭包的代码中,对于常规的foo()方法来说,按理说,在其内部的变量a(也就是var a = 1;)的内存空间应该在foo()方法执行完毕以后就消失了(原因是JS的函数执行机制:函数执行开始之后,JS给作用域内的变量分配内存空间,函数执行完成之后,空间自动回收),但是麻烦来了,foo()方法返回了一个新的方法bar(),而这个方法却访问到了foo()方法的变量a(原因是JS通过作用域链可访问到父级属性,这个大家都知道,你可能不知道的就是foo()方法在执行完毕之后,foo()方法的作用域链还存在),所以var a = 1;的内存空间还不能回收,所以说,闭包阻止了a的回收,方法bar()的存在延长了变量a的存在时间,相当于将变量a关闭在了自己的作用域范围内一样,只要方法bar()没有失效,那么变量a则会一直伴随着方法bar()存在。所以:方法bar()始终能引用上级作用域,这种引用被称为闭包。

闭包的显著特点:

  1. 函数套函数,所以至少有两层函数。
  2. 外层函数最少要干三件事:

闭包的技术原理就是利用作用域链是单向的这一特征。

那么闭包到底有个毛用呢?写简单易懂的代码会死么?闭包的三个好处:

1.希望一个变量长期驻扎在内存中
2.避免全局变量的污染
3.私有成员的存在

闭包的两个重要的使用场合就是:

模拟模块和私有属性,外层函数就是模块,外层函数内的变量就是私有属性

以上面的带闭包的代码举例,稍作修改:

function foo() {
    var a = 1;
    function bar() {
        a = a + 1;
        console.log(a);
    }
    return {bar:bar};
}

var closure = foo();
closure.bar(); // 2

foo()就是一个模块,调用一次foo()就创建了一个模块实例,closure就是模块实例。foo()作用域里面的a变量,你没有任何办法能直接访问它的值,这就形成了一种所谓的私有属性,当然JS里面是没有私有这个概念的,这里是模仿。关于模块的更多知识,其实属于“JS设计模式”的范畴,需要你看更多的资料了。

模拟块级作用域

通常用法是用一个自执行函数把异步函数包裹起来。比如,我有一个数组是[1,2,3,4,5,6,7],我想把它们分别作为参数,用ajax发送到服务器,然后分别获得打印的结果。简单的伪代码是:

var arr = [1,2,3,4,5,6,7];
for (var i = 0; i < arr.length; i++ ) {
    $.post('...', {d: i}, function(data) {
         console.log(i + ' : ' + data);
    });
}

结果你会发现,打印的i跟data不匹配,全乱了,并不会得到我想要的结果,原因涉及到JS异步机制,看我《对异步机制的理解》一文吧,这里不多解释。那么正确的闭包解决办法是什么?

var arr = [1,2,3,4,5,6,7];
for (var i = 0; i < arr.length; i++ ) {
    (function(i) {
        $.post('...', {d: i}, function(data) {
            console.log(i + ' : ' + data);
        });
    })(i);
}

也就是说,我利用自执行函数传递了一个参数,也就是i值,相当于7个自执行函数的作用域内都都放入了一个var i = 当前i值,所以OK了。

总结

所谓闭包,就是一个函数即使在词法作用域之外执行,也依然能记住并且能访问自己的词法作用域,这种用法就叫做闭包。

可能你已经在用闭包但却不知道,也可能没有在用闭包。其实要我说,闭包这个东西,用于面试题的意义最大,为了面试通过,你需要懂这里面的知识,但是,闭包比起面向对象、设计模式来,还是属于基础知识,没必要为了用闭包而刻意用闭包,重心还是放在面向对象编程和设计模式上为好。

上一篇 下一篇

猜你喜欢

热点阅读