你不知道的Javascript上卷中再次回顾闭包的概念
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数当前是在词法作用域之外执行
1 | function foo(){ |
函数bar的词法作用域能够访问foo的内部作用域,然后将bar函数本身当作一个值类型传递,在上面代码中,我们将bar所引用的函数对象本身当作返回值。
在foo()执行后,其返回值(也就是内部的bar函数)赋值给变量baz并调用baz(),实质上只是通过不同的标识符引用调用了内部的函数bar()
在foo()执行后通常会认为它的整个内部作用域会被销毁,因为JS引擎有垃圾回收释放内存的机制。然而,闭包的神奇之处就在于可以阻止作用域被销毁,因为bar函数仍在使用这个内部作用域,这个引用就叫闭包。
总结:
- 函数在定义时的词法作用域之外的地方被调用,而闭包使得函数可以继续访问定义时的词法作用域
- 只要是对函数类型的值进行传递,在函数被调用的时候,都可以看到闭包的身影
应用场景
setTimeout延时函数
1 | function wait(message){ |
将一个内部函数timer传递给setTimeout(),timer函数具有涵盖wait函数作用域的闭包,所以在函数外部,仍可以保有对message变量的引用
for循环和闭包
1 | for(var i=0;i<=5;i++){ |
对这段代码的预期是分别输出数字1~5,每秒一次,每次一个。当执行后发现,实际输出的效果是每秒一次的频率输出了5个6
因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。(这段代码的循环终止条件是i不再<=5,这时i首次成立的条件是i=6,而for循环中声明的i变量是全局变量,当打印事件发生的时候,i已经完成了赋值操作,即i=6,所以控制台打印的结果总是6)
这里没有达到我们预期效果的原因在于,我们试图假设循环中每次迭代在运行时都会给自己捕获一个i的副本,但根据作用域的工作原理,虽然循环中的五个函数是在各个迭代中分别定义的,但它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i
在循环的过程中每个迭代都需要一个闭包作用域,下面有两种解决办法:
- 创建一个变量用来在每次迭代中储存i的值(闭包的方式)
1
2
3
4
5
6
7
8for(var i=0;i<=5;i++){
(function(){
var j = i;
setTimeout(function timer(){
console.log(j);
}, j*1000);
})();
}
用立即执行函数,创造一个函数作用域。在每次i值改变的时候,都把i的值保存给内部函数作用域的变量j,因为立即执行函数跟i赋值的时候可以说是同步执行的,然后控制台的时候,就会拿到函数作用域中的i。
- let声明可以劫持块作用域,并在这个块作用域中声明一个变量
1
2
3
4
5for(let i=0;i<=5;i++){
setTimeout(function timer(){
console.log(i);
}, i*1000);
}
将let声明在for循环头部同样可以解决问题,它还有一个特殊的行为,这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
- 使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入
1
2
3
4
5for(var i=0;i<=5;i++){
setTimeout(function timer(j){
console.log(j);
}, i*1000, i)
}