JavaScript 解决循环打印问题

题目分析

以下代码运行后会打印出什么?

1
2
3
4
5
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}

答案:66666

虽然每个for循环中定时器设置的时间都是0,但由于 JavaScript 是单线程 eventLoop 机制,setTimeout是异步任务,遇到setTimeout函数时,JavaScript 会将其放入任务队列中,待同步任务执行完毕后,才执行任务队列中的异步任务。

又因为setTimeout函数也是一种闭包,往上找它的父级作用域链是window,而变量i是用var声明的,是window上的全局变量,所以此时变量i的值已经变成i = 6了,最后执行setTimeout时,当然会输出5个6了!

解决办法

如果就是要输入1 2 3 4 5,该怎么办呢?

立即执行函数

立即执行函数可以锁定参数值,传入每次循环的当前索引,从而锁定索引值。

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

使用 let 声明(块级作用域)

利用 JavaScript 的块级作用域,就不用这么麻烦了。如果 for 循环使用块级作用域变量关键字,循环就会为每个循环创建独立的变量,从而每次打印都会有正确的索引值。

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

定时器传入第三个参数

一般我们使用setTimeout都会使用2个参数,回调函数和延迟时间,但setTimeout是有第三个参数的。

一旦定时器到期,会将第三个参数作为参数传递给回调函数。这样打印时,也能得到正确的索引值。

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

使用 Promise

在任务队列tasks中,依次存放Promise,其中传入相应的i,异步操作完成之后,输出最后的i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const tasks = []; // 这里存放异步操作的 Promise
const output = i =>
new Promise(resolve => {
setTimeout(() => {
console.log(i);
resolve();
}, i);
});

// 生成全部的异步操作
for (var i = 0; i <= 5; i++) {
tasks.push(output(i));
}

// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
console.log(i);
});

版权声明

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!


JavaScript 解决循环打印问题
https://www.xukaiyyds.cn/posts/eeb964bd/
作者
xukai
发布于
2023年5月23日
更新于
2023年5月29日
许可协议