JS 函数的执行时机

189 阅读3分钟

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

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

先通俗的讲,上面代码执行时,先是for循环执行完,而setTimeout()内的回调函数在for循环执行完后执行6次,这个时候用来计数的i已经等于6了,所以会在控制台打印出6个6。

可是为什么setTimeout()能做到这一点?在网上查了一下,原因很复杂。

首先是因为没能正确理解setTimeout()。我们先来看下面的这段代码:

setTimeout(function(){
    console.log("here");
}, 0);
var i = 0;
//具体数值根据你的计算机CPU来决定,达到延迟效果就好
while (i < 900000000) {
    i++;
}
console.log("test");

结果为在过了一段时间之后,先打印了test,然后才是here。而且需要注意的是,上面的代码写的是setTimeout(..,0),如果按照之前错误地将setTimeout函数理解为延迟一段时间执行,那这里把时间赋为0岂不是马上执行了?而实验结论则印证了“setTimeout的意思是传递一个函数,延迟一段时间把该函数添加到队列中,并不是立即执行”的结论。(涉及到线程,异步,事件循环的知识我现在理解得还不到位,所以暂且不表)

再回到文章开头的代码,我们已经理解了console.log(i)在循环结束后执行6次。那为什么输出6呢? 跟闭包有关系。对闭包的解释:对函数类型的值进行传递时,保留对它被声明的位置所处的作用域的引用。

setTimeout()内的回调函数中的i并没有被声明,那就继续向外层的作用域找,我们可以看到i在最外层被声明。

console.log(i)中的i是全局作用域中的i,在循环结束后值为6,综上所述开头的代码会输出6个6。

2. 让上面代码打印 0、1、2、3、4、5 的方法

方法有很多,下面列举一些。

(1)将i的声明放入for循环内,利用let的特性

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

for 循环头部的let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

(2)引入IIFE(立即执行函数)

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

(3)利用setTimeout第三个参数

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

setTimeout()如果想给回调函数传递参数,直接在第二个参数delay后面加上附加的参数。

参考语法var timeoutID = scope.setTimeout(function[, delay, param1, param2, ...]);

(4)利用闭包

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