JS 函数的执行时机

294 阅读2分钟

众所周知,js的函数执行结果会随着它的执行时机不同而不同。但下面的代码难免让人匪夷所思:

为什么如下代码会打印 6 个 6?

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}

原因很简单,由于for循环的缘故,此代码中设定了6个setTimeout定时器。而因为 js 是单线程的,有一个事件队列机制,setTimeout 和 setInterval的回调会塞入事件队列中,排队执行。这就意味着定时器仅仅是计划代码在未来的某个时间执行,也就是执行完当前代码,才可能会开始执行定时器里面的代码。所以,当for循环执行完成后,i的值为6,再执行设定好的6个定时器,于是输出了6个6。

而下面的代码则会打印出0、1、2、3、4、5

for(let i=0;i<6;i++){
  setTimeout(()=>{console.log(i)},2000)
}

这又是为什么呢?原因是let是块级作用域,let写在了for语句中,所以作用域是for代码块。所以每一次 for 循环,console.log(i); 都引用到 for 代码块作用域下的i,因为这样被引用,所以 for 循环结束后,这些作用域在 setTimeout 未执行前都不会被释放。

如果不相信,可以在for循环体外打印i;你会发现报错: Uncaught ReferenceError: i is not defined

也就是说,上面的代码相当于:

let i;
for(i=0;i<6;i++){
  let temp=i;
  setTimeout(()=>{console.log(temp)},2000)
}

那用var可不可以输出0、1、2、3、4、5?

答案是不可以。

for (var i = 0; i < 6; i++) { 
    setTimeout(function (){
        console.log(i);  
     },1000);  
}

因为因为 setTimeout 的 console.log(i); 的i是 var 定义的,所以是函数级的作用域,不属于 for 循环体,属于 global。等到 for 循环结束,i 已经等于 6了,这个时候再执行 setTimeout 的五个回调函数(参考上面对事件机制的阐述),里面的 console.log(i); 的 i 去向上找作用域,只能找到 global下 的 i,即 6。所以输出都是6。

如果你不相信,可以在for循环体外打印i,你会发现,i存在,且i=6。

除了使用 for let 配合,还有什么其他方法可以打印出 0、1、2、3、4、5?

用立即执行函数:

for (var i = 0; i < 6; i++) { 
    (function(i){      //立刻执行函数
        setTimeout(function (){
            console.log(i);  
         },1000);  
    })(i);  
}

这样就把i的作用域放在for循环体里面了。执行结果为:0、1、2、3、4、5。