说起闭包,就想起来探寻闭包的艰辛路程,为了彻底理解 “闭包” 我读过很多文章,但是能把闭包讲的很透彻的,真的很少。所以写这篇文章的初衷就是为了拨开闭包神秘的面纱,探寻闭包的 “真相” 。
理解闭包
- 《javaScript高级程序设计》中的解释:
闭包是指有权访问另一个函数作用域中变量的函数。
- 《你不知道的Javascript》中的解释:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope. 闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行也是如此。
我们边思考定义边看例子:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
这个例子,在foo()内部声明了一个bar函数,该函数持有对foo()内部变量 a 的引用(该函数记住并访问了其词法作用域),因此我们就可以说:bar()具有一个涵盖foo()作用域的闭包,但bar()是在其所在的词法作用域内执行的,所以当foo()执行完,foo()的整个作用域都被销毁,这样的闭包就毫无意义。我们稍微改一下这段代码,看一下效果:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); //2
在这个例子中,我们没有在foo()内部执行bar(),而是把bar当作一个返回值。在foo()外部用变量baz接收,并执行baz()(其实就是执行了bar()),正确输出了 a 的值,这样就实现了**bar()在其词法作用域之外执行,并且还能记住并访问其词法作用域**,即实现了闭包。
在foo()执行后,如果没有被引用,js引擎中的垃圾回收器就会销毁掉foo()中所有变量。而闭包的“神奇”之处正是可以阻止foo()中变量被销毁。这里bar被外部引用,使得foo()作用域能够一直存活,以供bar()在之后任何时间进行引用。bar()依然持有对foo()作用域的引用,而这个引用就叫作闭包。
循环中的闭包
我们看下面的例子,思考一下运行时输出的结果。
for (var i=1; i<=5; i++) {
setTimeout(
function timer () { console.log( i );},
i*1000
);
}
正常情况下,我们想要用这段代码实现:每秒输出一个数字,分别为1,2,3,4,5。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。这是为什么呢?
首先我们来看一下这个 6 从何而来:在js中setTimeout(fn,time)为一个延迟(异步)函数,这个函数的本意是:延迟time毫秒后执行fn()并在延迟过程中先执行后面的代码,而接下来for循环会被一瞬间执行完,所以此时setTimeout当中timer函数所引用的i(全局作用域的 i )为循环结束的 i 此时 i = 6,而setTimeout的第二个参数在传参的时候就已经把实参的值赋给了形参,所以结果就是以每秒一次的频率输出五次 6。
那么问题来了,我们现在怎么实现:每秒输出一个数字,分别为 1,2,3,4,5 ?
我们试一下这个方案:用一个函数把setTimeout()包起来形成一个局部的作用域,然后把 i 的值保存下来,让setTimeout()去引用这个 i ,像下面这样:
for (var i=1; i<=5; i++) {
function foo (i) {
setTimeout(
function timer () { console.log( i );},
i*1000
);
}
foo(i);
}
这段方案实现了我们上面的期望,由于setTimeout()的特性,它会在foo函数执行完之后再执行timer(),而此时timer()所引用的 i 是foo()局部作用域中的 i (它的值是foo()执行时,外部实参 i 赋值给它的),这个例子就用到了闭包,实现了让循环中的 i 持久化。
当然我们也可以用 块级作用域+闭包 来实现这个效果:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout(
function timer () { console.log( j );},
j*1000
);
}
这里let声明的变量 j 只作用于当前for循环的 {} 块中,它保存了当前 i 的值,以供当前setTimeout()使用。
但是我们通常这么使用:
for (let i=1; i<=5; i++) {
setTimeout(
function timer () { console.log( i );},
i*1000
);
}
但是在使用过程中我就有这样的疑问:js文档中for(let i=1; i<=5; i++)的let i=0为变量初始化,只在循环开始之前执行一次,那它是如何实现每个{}中存在一个唯一的变量 i 的?查询了很多资料,为了实现上面的效果,js内部做了类似这样的处理:
{
let i;
for (i=1; i<=5; i++) {
let j = i;
setTimeout(
function timer () { console.log( j );},
j*1000
);
}
}
总结
这篇文章只是去解释和了解闭包以及特殊形式的闭包(例如setTimeout()这样的异步函数中的闭包)。在我看来,闭包地意义不在于你知道什么时候去使用名叫“闭包”的东西,而是在于你使用了某种方式实现了闭包的特性:变量的持久化。