JS闭包学习笔记(2):循环和闭包

时间:2022-09-29 22:44:09

经典例子:

for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}

最终的结果并不如我们期待的打印出1,2,3,4,5,而是打印出6,6,6,6,6.

setTimeout的回调函数timer会在循环结束后才开始执行,因此会输出i最后的值6.即使将setTimeout的第二个参数设为0,timer还是会在循环结束后才执行,因此还是会打印6.

我们本来期待每一次循环中都能保留当时的i,但当闭包起作用时,虽然每个函数是在各次循环中依次定义的,但它们最后都share同一个作用域,即全局作用域,因此都使用同一个i。五个函数其实只是一个接一个地被声明,其中并没有循环存在了。

如何改进呢?
首先想到使用IIFE(立即执行函数):

for(var i=1;i<=5;i++){
(function(){
setTimeout(function timer(){
console.log(i);
},i*1000)
})();
}

IIFE的确增加了一层作用域,每次setTimeout执行都会闭包其当前循环中的IIFE作用域,此时的确每个timer函数闭包了自己的作用域,但IIFE是空的,最后导致每一个timer还是会调用同一个全局变量i,值为6.

从空的IIFE进行改进:每个函数都需要它自己的变量,因此要在在每次循环中保存当时的i:

for(var i=1;i<=5;i++){
(function(){
var j = i;
setTimeout(function timer(){
console.log(j);
},j*1000)
})();
}

这回才得出正确的结果!

另一种写法:

for(var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000)
})(i);
}

每一次循环中的IIFE为当次循环创建了一个新的作用域,这样其内部的setTimeout回调函数可以闭包这个新作用域,这个作用域有保存了当时i的值的变量j,因此每一个回调函数可以使用这个j并输出正确结果。

在每次循环中IIFE创建了一个新的作用域,换句话说,我们需要一个基于每次循环的块作用域。let关键字可以拦截一个块,并在此块中声明一个变量,此块就可以变成闭包的作用域,因此下面的代码也可以正确解决问题:

for(var i=1;i<=5;i++){
let j=i; //为闭包创建了一个块作用域
setTimeout(function timer(){
console.log(j);
},j*1000);
}

甚至可以直接把let关键字作用于for循环的头部,此时let声明的变量不是在循环中只声明一次,而是在每次循环时都会被声明,而且被初始化为上次循环结束时的值:

for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}