这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战
介绍
闭包就是能够读取其他函数内部变量的函数。在JS中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
闭包的产生
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo() {
let a = 1;
function bar() {
console.log( 1 ); // 1
}
bar();
}
foo();
以上代码并不是一个的闭包,函数必须在另一个上下文中调用另一个上下文的变量。
在顶级作用域中定义了一个foo函数并执行,在foo函数内定义了一个bar函数,bar函数打印了定义在foo词法作用域的a变量,但自己也处于该作用域中。因此js引擎在解释这段代码时,是根据作用域链逐层往上查询变量a是否存在。
function foo() {
let a = 1;
return function () {
console.log( a );
}
}
var baz = foo();
baz();
这就是闭包,baz变量被赋值为一个函数的引用,该函数定义在foo函数内部,并且使用了foo词法作用域中的变量a,完美符合了闭包的定义。
由于垃圾回收机制,foo函数在执行完毕后,其作用域会被销毁,因此内部定义的变量a及函数bar也会被销毁,但执行结果显示它们依旧存在,这是因为存在闭包。
顶级作用域中的baz引用了foo词法作用域中返回的函数,相当于foo函数变相地被引用了,这使得引擎不会回收foo的词法作用域,这形成了闭包,使得bar函数可以继续访问定义时的词法作用域。
闭包使得函数可以继续访问定义时的词法作用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn();
}
把内部函数 baz 传递给 bar函数调用。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar(); // 2
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。
识别闭包
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
1秒后会输出一条提示信息,而这条信息是在1秒前传入,并且存在于wait的词法作用域中,它被使用是在timer的词法作用域,根据引擎查询变量的规则,他会根据作用域链逐层向上查询,这里它的确查询到了,说明wait执行往后依旧存在,这都是闭包造成的。
本质上如果将函数当作参数传递,就会造成闭包。例如在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
循环与闭包
要说明闭包,for 循环是最常见的例子。
//实际结果是每隔一秒打印一个 '6'
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
代码分成2部分,循环部分是同步执行代码,定时器部分是异步代码。
在for循环中使用var定义变量i不会形成块作用域,因此i的作用域是顶级作用域,在循环结束后,i依旧才存在,并且值为6。
异步部分,timer函数与for循环所形成的块之间构成了一个闭包,并引用了变量i,此时的i并不是每次循环时i的复制,而是循环结束后的i。
根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
这是因为在使用var定义的变量不存在块级作用域。
通过IIFE创建新的作用域。
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
每次循环,IIFE都会创建一个独立的作用域,并将外层作用域的i都回被保存在自己的作用域中,以供timer函数的闭包调用。
更加简洁的实现
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
//利用let来形成块作用域
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
总结
函数可以记住并访问被定义时所在的词法作用域,因此当函数在当前词法作用域之外执行,可以访问之前作用域的变量和方法,这就是闭包。