循环和闭包
2021.03.18 Thu
1
2
3
4
5
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 是异步事件,当 js 遇到异步事件时会把异步事件放到执行队列里,于是每一次 for 循环遇到的 setTimeout 都被放到了执行队列等待执行,但是因为 for 循环的阻塞机制,在执行队列里的所有 setTimeout 将不得不等到 for 循环结束才能回到主线程里。

而等到 for 循环结束,i 的值已经变成了 6.尽管每次循环都定义了一个 setTimeout,但是它们都被 for 循环封闭在了一个共享的全局作用域中,它们实际上摸到的 i 值其实都是一个 i,所以打印出来的都是 6.


将代码改写

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

这个时候代码终于如愿以偿地实现了人类无聊的初衷,每隔一秒地打印1 2 3 4 5

上面把每一次 for 循环的执行操作写成了立即执行函数,在循环过程中每个迭代都生成了一个闭包作用域,于是 setTimeout 的每一次回调都可以看成一个闭包,它可以一直访问它所在作用域里的变量。


随着时代的发展,代码可以是这个样子

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

let 可以用来劫持块作用域并且在这个块作用域中声明一个变量。

for 循环头部的 let 声明还会有一个特殊的行为,这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

检测到页面内容有更新,是否刷新页面