JS 函数的执行时机

185 阅读3分钟

for循环里的定时器

先看一段代码。

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

熟悉这道题目的人立马可以说出答案:

6
6
6
6
6
6

但是很多新手第一次看到这段代码时会产生一个错觉,认为打印结果会是0,1,2,3,4,5。

为什么明明定时器的时间设置为了0,定时器却在console.log('a')这句代码运行了之后才运行?

原来在 js 的世界里,定时器是一个异步任务

这里有一个形象的比喻:

当你在打游戏时,妈妈喊你出去吃饭,你说:”马上就来”,你现在有两个选择:1. 关掉游戏,不管其他四个队友 2. 继续打团,结束游戏后立马去吃饭。这时,你手头上的游戏就像一系列同步任务,妈妈的催促就像定时器。

而 JS 会优先执行当前的同步任务,在同步代码执行结束后才会去处理任务队列中的异步任务

这样,即便定时器设置了0,也是在忙完手头的事情之后才会去读取任务队列

因此在所有同步代码执行完毕之后,for循环里的i值早已变成了5,循环已经结束。注意,for循环的圆括号部分也是同步代码。

这就是为什么打印出来5个6,而不是0,1,2,3,4。


总结一下,JS 里所有任务可以分成两种,一种是同步任务,另一种是异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"的任务,只有**"任务队列**"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

let 关键字和立即执行函数

如果想实现for循环里的定时器打印出0,1,2,3,4,可以使用ES6的 let 关键字。

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

let 关键字劫持了 for 循环的块作用域,产生了类似闭包的效果。

在使用 let 声明变量 i 时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量,每个 setTimeout 引用的都是不同的变量实例,所以遍历出来的是符合新手期望的值,也就是循环执行过程中每个迭代变量的值。

let 可以实现块作用域的效果,但是它是 ES6语法,在低版本语法的时候如何生成块作用域?

答案是:使用try...catch语句。

for(var i = 0; i < 5; i++) {
    try {
        throw(i)
    } catch(j) {
        setTimeout(function () {
            console.log(j);
        });
    }
}

除此之外,还可以使用立即执行函数。

for(var i = 0; i < 5; i++) {
    !function(i) {
        setTimeout(function () {
            console.log(i);
        });
    }(i)
}

利用闭包的原理,闭包使一个函数可以继续访问它定义时的作用域。而这个新生成的作用域将每一次循环的当前i值单独保存了下来。