闭包的英文是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()始终能引用上级作用域,这种引用被称为闭包。
闭包的显著特点:
- 函数套函数,所以至少有两层函数。
- 外层函数最少要干三件事:
- 提供至少一个局部变量,注意,我没有说“定义变量”,因为变量除了定义,还有一种情况是通过参数传递进来,不需要定义。这个变量在外层函数执行完毕之后不能被销毁,销毁就不叫闭包。
- 定义至少一个内层函数。
- 想办法将内层函数返回给调用外层函数的作用域里的变量或者函数。通常是用
return
。 - 内层函数至少要干一件事:定义真正的执行语句。用的时候用内层函数。
闭包的技术原理就是利用作用域链是单向的这一特征。
那么闭包到底有个毛用呢?写简单易懂的代码会死么?闭包的三个好处:
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了。
总结
所谓闭包,就是一个函数即使在词法作用域之外执行,也依然能记住并且能访问自己的词法作用域,这种用法就叫做闭包。
可能你已经在用闭包但却不知道,也可能没有在用闭包。其实要我说,闭包这个东西,用于面试题的意义最大,为了面试通过,你需要懂这里面的知识,但是,闭包比起面向对象、设计模式来,还是属于基础知识,没必要为了用闭包而刻意用闭包,重心还是放在面向对象编程和设计模式上为好。