我正在参加「掘金·启航计划」。
下边这个经典面试题,乍一看,输出 0 1 2 3 4。其实不然,这里输出的是 5 个 5。
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
这个问题涉及到了 JavaScript 的作用域、变量提升与同步/异步。下面让我们详细解析一下这个问题。
面试题解析
不想看字的,可以直接看图,一图胜千言。
首先,确认下,这里for语句中用var声明的变量i不是for循环的局部变量,是跟for在相同的作用域中的变量(因为var没有块作用域,在块语句中形成不了块作用域)。
for循环是同步的,setTimeout是异步的。这里会生成5个setTimeout,每个setTimeout的callback都会先放到异步队列中去,等到for循环同步执行完之后才会依次执行。
这里的i是全局作用域中的同一个i,i在每次for循环的时候值都会加1,一直加到5判断不满足条件则终止for循环(此时的i=5)。同步任务执行完成,开始依次执行异步队列中setTimeout callback,打印5个5。
如果使用let声明变量i,i是for语句的局部变量,由于let可以在块语句中形成块级作用域,在循环的时候,每次的循环体都有自己的局部变量i。所以会依次输出0 1 2 3 4。
for循环中()小括号部分是一个父作用域。{}块语句部分是一个子作用域,子作用域每次循环都会生成一个。
如何让其输出01234?
1、将var改为let
for(let i=0; i<5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
2、使用闭包
使用IIFE将变量i作为参数传入,形成闭包,保证每次的变量i不会被污染。
for(var i=0; i<5; i++) {
(function(j){
setTimeout(function () {
console.log(j);
}, 1000);
})(i)
}
3、使用setTimeout的第三个参数(这里其实也形成了闭包)。
for(let i=0; i<5; i++) {
setTimeout(function (j) {
console.log(j);
}, 1000, i);
}