循环打印问题是面试中经典的题目,一般会给出代码,让我们解释原因,并给出若干解决方案。

1. 题目分析

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

答案:6 6 6 6 6

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

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

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

2. 解决办法

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

1. 立即执行函数

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

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

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

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

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

3. 定时器传入第三个参数

一般我们使用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
);
}