前言
本文于2021.11.12全面更新。
一段有点难理解的代码
let i = 0
for(i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
实际面试过程中更常看到的版本是完全不考虑ES6语法的:
for(var i = 0; i < 6; ++i) {
setTimeout(function() {
console.log(i);
}, 0);
}
第一眼看到代码时,我们会想当然地觉得会输出0-5
(0到5)。但最终的输出结果却是6个6。如下图:
完全不考虑ES6的情况下会输出如下图:
原因如下:
- 假设在不懂宏任务和微任务概念的情况下,
setTimeout(cb, t)
应被理解为在t
时间过后尽快执行cb
函数。换句话,t
时间过后才有cb
函数执行的可能性。 - 引入宏任务和微任务概念之后,可以见下一个小节。
用事件循环机制解释为什么会输出6个6
事件循环机制的顺序是:调用栈 -> 微任务 -> 宏任务
。
- 普通函数、
setTimeout()
函数本身、Promise
构造函数本身会进入调用栈 Promise
后的then
、catch
、finally
回调函数会进入微任务队列setTimeout
中的回调函数会进行宏任务队列
以var
版本执行方式为例,说明执行过程。在上一小节代码中,在for
循环开始后,执行步骤如下:
- 执行循环5次,每次都往调用栈中压入
setTimeout()
函数本身。在i === 6
时跳出循环; - 每次
setTimeout()
函数进调用栈的过程,必然伴随着function () { console.log(i) }
进入宏任务队列; - 然后按照
调用栈 -> 微任务 -> 宏任务
执行一栈两队列中的函数; - 调用栈中的
setTimeout()
函数被执行完毕,必须要等到0 * 5ms
之后执行后续的步骤; - 由于此题中没有
Promise()
,所以跳过微任务队列; - 宏任务队列中有6个
console.log(i)
。由于此题中i
是全局变量,值为6,所以自然会输出6个6。
如何在setTimeout之后还能输出0-5
很简单,直接使用let
在for
循环中即可。修改代码如下:
for(let i = 0; i < 6; i++){
setTimeout(() => { console.log(i); }, 0);
}
这样相当于每次进去的都是新变量。但上述写法是个语法糖(这在别的语言中非常正常的写法变成了语法糖...无语,谢天谢地ES6让JavaScript看起来正常了许多)。如果不考虑语法糖,可以这样写:
let i = 0;
for(i = 0; i < 6; i++){
let _ = i;
setTimeout(() => {
console.log(_);
}, 0);
}
输出如下:
某亦庄大厂的考法是这样的:
如果限定条件为禁止使用let,乃至禁用ES6,则可以这样改用类lambda
表达式(即立即执行函数):
for(var i = 0; i < 6; ++i){
(function(i) {
setTimeout(function() {
console.log(i);
}, 0);
})(i);
}
输出如下: