先看代码
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
输出结果为:打印10次10
实际上,var定义的变量在 for循环之外是可以访问到的,for 循环是同步操作,SetTimeout是异步。
执行步骤如下
1.for 循环(同步操作)快速完成所有 10 次迭代。
2.在每次迭代中,它设置了一个 setTimeout(异步操作)。
3.循环结束后,i 的值变为 10。
4.大约 1 秒后,事件队列中的 setTimeout 回调函数开始执行。
5.每个回调函数执行时,它们都引用同一个 i,而这时 i 的值已经是 10 了。
解决方案1:let定义块级作用域
for(let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
原因
1.块级作用域:
let
声明的变量具有块级作用域。在 for 循环中,每次迭代都会创建一个新的块级作用域。这意味着每次循环都会创建一个新的i
变量。2.闭包: 每个 setTimeout 的回调函数都形成了一个闭包,它可以访问到创建它时的作用域中的变量。
3.循环迭代中的行为: 使用
let
时,JavaScript 引擎会在每次循环迭代中创建一个新的i
变量。每个 setTimeout 的回调函数都会捕获到当时迭代中的i
值。
执行步骤如下
1、for 循环(同步操作)快速完成所有 10 次迭代。
2、在每次迭代中:创建一个新的块级作用域,包含当前迭代的 i 值。
设置一个 setTimeout(异步操作)。
setTimeout 的回调函数形成一个闭包,捕获当前迭代的 i 值。
3、循环结束后,外部的 i 不再可访问(因为 let 创建的是块级作用域)。
4、大约 1 秒后,事件队列中的 setTimeout 回调函数开始执行。
5、每个回调函数执行时,它们引用的是各自闭包中捕获的 i 值,这些值分别是 0 到 9。(
let
的块级作用域特性确保了每个迭代的i
值被正确地"冻结"在各自的闭包中。)
解决方案2:IIFE立即执行
for(var i = 0; i < 10; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, 1000);
})(i);
}
执行步骤:
1.for 循环快速执行 10 次迭代(同步操作)。
2.在每次迭代中:创建一个新的IIFE,并立即执行它(同步操作)。
IIFE接收当前的 i 值作为参数。
在IIFE内部,设置 setTimeout(异步操作)。
3.循环结束。约 1 秒后,setTimeout 回调开始执行,但每个回调都能访问到创建它时IIFE参数中的 i 值。
效果:1s后几乎同时打印0-9
解决方案3:Promise
const tasks = [];
for (var i = 0; i < 10; i++) {
(function(i) {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(i);
resolve();
});
}));
})(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i); //i在for之外可被访问
}, 1000);
});
执行步骤:
1、创建空数组 tasks。
2、for 循环开始(同步操作):循环 10 次(i 从 0 到 9)。
每次迭代都创建并立即执行一个 IIFE。
3、在每次 IIFE 执行时(同步操作):创建一个新的 Promise,在 Promise 内部设置一个 setTimeout(异步操作)。
将这个 Promise 推入 tasks 数组。
4、循环结束,此时 i 的值为 10。
5、调用 Promise.all(tasks)(异步操作):等待 tasks 数组中的所有 Promise 解决。
6、设置 .then() 回调(异步操作):当所有 Promise 解决后,这个回调会被加入到事件队列。
7、主线程的同步操作全部完成,开始处理异步任务:10 个 setTimeout 回调几乎同时开始执行。
每个回调打印出对应的 i 值(0 到 9),并解决对应的 Promise。
8、所有 Promise 解决后,Promise.all 的 .then() 回调执行:设置另一个 setTimeout,延迟 1000 毫秒。
9、大约 1 秒后,最后的 setTimeout 回调执行:打印 i 的值,此时 i 为 10。
效果就是:几乎同时打印出 0 到 9,大约 1 秒后,打印出 10
方案3的更简洁的写法
new Promise((resolve) => {
for (var i = 0; i < 10; i++) {
console.log(i);
resolve();
}
});
因为没有定时器,不存在异步操作,所以直接打印0-9;
resolve()
在第一次迭代就被调用,但这并不会中断循环的执行,函数会继续执行直到完成或遇到 return
语句。