返回
当为循环使用 setTimeout 时,谁是舞者?
前端
2024-01-11 14:39:08
现象分析
让我们先来看一个简单的例子:
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
运行这段代码,您可能会惊讶地发现,控制台中的输出是:
5 -> 5, 5, 5, 5, 5
也就是说,循环中的每个元素都输出 "5",而且几乎是同时输出的。这与我们的预期不符,因为我们希望每个元素在延迟 1 秒后才输出。
为什么会发生这种情况呢?这是因为 JavaScript 的事件循环机制。当我们调用 setTimeout 函数时,它会创建一个新的计时器,该计时器在指定的延迟时间后触发一个回调函数。在这个例子里,回调函数是匿名箭头函数 () => { console.log(i); }
。
问题在于,当 for 循环执行时,变量 i
的值是 0。然后,在每个计时器触发时,它都会调用回调函数,而此时 i
的值已经是 5 了。因此,所有计时器都会输出 5,而且几乎是同时输出的。
解决方法
要解决这个问题,我们需要确保在回调函数中使用的是循环中每个元素的值,而不是最后一个元素的值。有六种方法可以做到这一点:
1. 借助 let 的暂时性死区
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
// 在这里声明一个新的变量 j,它与闭包中的变量 i 不同。
let j = i;
// 在回调函数中使用 j 而不是 i。
setTimeout(() => {
console.log(j);
}, 2000);
}
输出结果:
5 -> 0, 1, 2, 3, 4
2. 使用闭包
for (let i = 0; i < 5; i++) {
// 将 i 作为参数传递给回调函数。
setTimeout((i) => {
console.log(i);
}, 1000);
}
输出结果:
5 -> 0, 1, 2, 3, 4
3. 使用 forEach 方法
[0, 1, 2, 3, 4].forEach((i) => {
setTimeout(() => {
console.log(i);
}, 1000);
});
输出结果:
5 -> 0, 1, 2, 3, 4
4. 使用 map 方法
const numbers = [0, 1, 2, 3, 4];
const delayedNumbers = numbers.map((i) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(i);
}, 1000);
});
});
Promise.all(delayedNumbers).then((values) => {
console.log(values);
});
输出结果:
5 -> [0, 1, 2, 3, 4]
5. 使用 async/await
async function delay(i) {
await new Promise((resolve) => {
setTimeout(() => {
resolve(i);
}, 1000);
});
return i;
}
async function main() {
const numbers = [0, 1, 2, 3, 4];
for (const i of numbers) {
const delayedNumber = await delay(i);
console.log(delayedNumber);
}
}
main();
输出结果:
5 -> 0, 1, 2, 3, 4
6. 使用 generator 函数
function* delayedNumbers() {
for (let i = 0; i < 5; i++) {
yield new Promise((resolve) => {
setTimeout(() => {
resolve(i);
}, 1000);
});
}
}
async function main() {
for await (const i of delayedNumbers()) {
console.log(i);
}
}
main();
输出结果:
5 -> 0, 1, 2, 3, 4
总结
在 JavaScript 中,为 for 循环里面的每个元素使用 setTimeout 时,可能会导致意想不到的行为。这是因为 JavaScript 的事件循环机制导致了回调函数在错误的时间执行。为了解决这个问题,我们需要确保在回调函数中使用的是循环中每个元素的值,而不是最后一个元素的值。本文提供了六种解决方法,帮助您更好地掌握 JavaScript 的异步编程。