首先什么是闭包?
《JavaScript高级程序设计》这样描述闭包:
闭包是指有权访问另一个函数作用域中的变量的函数。
《你不知道的JavaScript》这样描述:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
讲解闭包
上面已经说到,当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 分析下面一段代码:
function fn1() {
var name = 'Apple';
function fn2() {
console.log(name);
}
fn2();
}
fn1(); // Apple
根据《JavaScript高级程序设计》和《JavaScript权威指南》来说,上面的代码已经产生闭包了。fn2访问到了fn1的变量,满足了条件“有权访问另一个函数作用域中的变量的函数”,fn2本身是个函数,所以满足了条件“所有的JavaScript函数都是闭包”。
这其实并不是闭包,fn2执行时实在fn1内部通过词法作用域找到name的值并打印。 再看一段代码:
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
这样就清晰地展示了闭包:
-
fn2的词法作用域能访问fn1的作用域
-
将fn2当做一个值返回
-
fn1执行后,将fn2的引用赋值给fn3
-
执行fn3,输出了变量name
我们知道通过引用的关系,fn3就是fn2函数本身。执行fn3能正常输出name,这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。
正常来说,当fn1函数执行完毕之后,其作用域是会被销毁的,然后垃圾回收器会释放那段内存空间。而闭包却很神奇的将fn1的作用域存活了下来,fn2依然持有该作用域的引用,这个引用就是闭包。
总结:某个函数在定义时的词法作用域之外的地方被调用时产生闭包,闭包可以使该函数访问定义时的词法作用域。
注意:对函数值的传递可以通过其他的方式,并不一定值有返回该函数这一条路,比如可以用回调函数:
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
fn3(fn2);
}
function fn3(fn) {
fn();
}
fn1();
本例中,将内部函数fn2传递给fn3,当它在fn3中被运行时,它是可以访问到name变量的。
所以无论通过哪种方式将内部的函数传递到所在的词法作用域以外,它都持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。
再次解释闭包
观察以下代码:
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
在这段代码中,我们对其的预期是输出1~10,但却输出10次11。这是因为setTimeout中的匿名函数执行的时候,for循环都已经结束了,for循环结束的条件是i大于10,所以当然是输出10次11咯。
究其原因:i是声明在全局作用中的,定时器中的匿名函数也是执行在全局作用域中,那当然是每次都输出11了。
原因知道了,解决起来就简单了,我们可以让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值。
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
闭包的应用
闭包的应用比较典型是定义模块,我们将操作函数暴露给外部,而细节隐藏在模块内部:
function module() {
var arr = [];
function add(val) {
if (typeof val == 'number') {
arr.push(val);
}
}
function get(index) {
if (index < arr.length) {
return arr[index]
} else {
return null;
}
}
return {
add: add,
get: get
}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));