闭包的定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的。
闭包的理解
上述对闭包的描述貌似有点难以理解,不过问题不大,我们通过下面的段代码来理解,就明白是什么意思了。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz();
上述代码中,函数 bar() 的词法作用域能够访问 foo() 的内部作用域,因为函数bar引用了函数foo的变量a。然后我们将 bar() 函数本身当作 一个值类型进行传递,当做函数foo()的返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),执行函数baz()实际上是调用了foo()内部的函数 bar(),这就是定义中的“函数是在当前词法作用域之外执行”,即函数bar()在foo()内部定义,在foo()外部调用。
一般情况下,当一个函数执行完之后,函数内部的作用域会被销毁,js引擎的垃圾回收器会释放函数内的不再使用的内存空间。然而上述代码中,当函数foo()执行完之后,其内部的作用域未被销毁,因为函数bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。闭包使得函数可以继续访问定义时的词法作用域。
闭包的运用
回调函数中的闭包
闭包虽然看似神秘,但是我们在平时的开发中已经或多或少地使用了闭包,只是我们没有发觉而已,比如类似于如下代码:
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
上述代码中,将一个内部函数(名为 timer)传递给 setTimeout(…)。此时timer 具有涵盖 wait(…) 作用域的闭包,因此还保有对wait()的变量 message 的引用。在wait(…) 执行 1000 毫秒后,它的内部作用域并不会被销毁,因为timer 函数依然保有 wait(…) 作用域的闭包。
在定时器、事件监听器、 Ajax 请求等任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
for循环与闭包
我们来考察一下如下代码:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
正常情况下,我们对上述代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。
我们明明执行了五次函数,并且每次都是打印i,按理说i每次都是不同的,但最终结果是每次打印的i都是6,这是为什么呢?一个合理的解释是虽然这五个函数是在各个迭代中分别定义的, 但是它们其实都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
那么怎么样才能使得在每一次迭代(循环)中,timer函数引用的是不同的i呢?答案是使用闭包。我们改造一下代码:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
上述代码中,我们在for循环的作用域中使用let声明了一个变量j保存每一次循环的i(使用let声明变量实际上在for循环内部形成了了一个块级作用域,这是es6中let的特性,不太了解的朋友可以去看看es6),这时timer()函数就引用了for循环作用域中的j,形成闭包。
闭包的缺点
使用闭包可以帮我们解决很多问题,但闭包的缺点是导致内存泄露,因为闭包会一直保持着对某些本该释放的作用域的引用,因而作用域内的变量所占用的内存不会被释放。