你不知道的Javascript:有趣的setTimeout

今天在回顾JavaScript进阶用法的时候,发现一个有趣的问题,话很少说,先上代码:javascript

for(var j=0;j<10;j++){
      setTimeout(function(){console.log(j)},5000)
    }
复制代码

看到这三行代码,也许你会不耐烦道:又要讲闭包?要吐了好么?别急,让咱们先来思考一下,这段代码在浏览器中的执行结果是什么?java

甲:顺序打印0到9?浏览器

乙:这题我见过,打印十个10!闭包

哪一个答案正确?咱们继续上图:异步

执行结果显示,浏览器打印出了十个10(由于图片处理的缘由,按下回车到打印以前其实间隔了5秒左右),貌似乙胜出了。但若是你足够细心,你会发现几个问题:

  1. 为何会循环打印十个10而不是0到9?
  2. 从结果来看,for循环执行完跳出以后,才开始执行setTimeout(因此j才等于10),为何不是每次迭代都执行一次setTimeout呢?

若是上述三个问题你都能回答上来,恭喜你,你已经开始掌握了JavaScript深层次的知识,若是不能,那就乖乖往下看吧!async

为何会循环打印十个10函数

许多人习惯用第二个问题中的执行结果来回答这个问题:“for循环执行完跳出以后,才开始执行setTimeout,因此才打印了十个10”。这样的答案,只能说是既应付了本身,又应付了别人。其实,要解答第一个问题,首先要解答的就是第二的问题。性能

为何不是每次迭代都执行一次setTimeout

你们都知道,JavaScript在ES6出现之前,是没有块状做用域的,这就意味着, 在for循环中用var定义的变量j,实际上是属于全局的,即在全局范围内均可以被访问到,既然如此,那其实整个全局做用域中就只有一个j,每次for循环都i是在更新这个j。优化

那么如今关键的问题在于,为何整个for循环会先于setTimeout执行,而不是咱们正常理解的,一次迭代执行一次。ui

这就涉及到了JavaScript的核心特性:单线程。

JavaScript设计的初衷,是浏览器用来与用户进行交互和DOM操做的。这就决定了它必须是单线程的,设想JavaScript同事有两个线程,一个线程在DOM节点添加内容,一个线程删除该节点,浏览器就会出现混乱。因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。

单线程就意味着,全部任务须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就不得不一直等着。

为了优化单线程的性能,JavaScript将任务分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有主线程中的同步任务执行完毕,异步任务才会进入执行队列执行。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

而setTimeout,就被JavaScript定义为异步任务。每次for循环的迭代,都将setTimeout中的回调函数加入任务队列等待执行。也就是说,只有同步任务中的for循环彻底结束,主线程中才会去任务队列中找到还没有执行的十个setTimeout(十次迭代)回调函数并顺序执行(先进先出)。而此时,i已经通过循环结束变成了10,因此,此时主线程执行的,是十个一摸同样的打印i的回调函数,即打印十个10。至此就完美回答了第一和第二个问题,文章开头的代码与下面的代码实际上是等价的:

for(var i=0;i<10;i++){}
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
复制代码

小小的一个setTimeout,牵扯出了不少JavaScript的深层次问题,虽然总结成一篇文章只有区区数百字,可是我在成文的过程当中查阅了大量的资料,也作了许多实验。

最后,给出一个很小可是仍然在困扰个人一个问题,但愿有兴趣的小伙伴能够跟我一块儿研究:

setTimeout(function(){while(true){}},6000);
    setTimeout(function(){console.log(1)},10000);
    setTimeout(function(){console.log(2)},5000);
复制代码

上述代码的执行顺序是怎样的?setTimeout的定时,是定时插入执行栈以后当即执行,仍是当即插入执行栈定时执行?

期待你们的留言。

相关文章
相关标签/搜索