资讯专栏INFORMATION COLUMN

你不知道的Javascript:有趣的setTimeout

whjin / 2027人阅读

摘要:单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。为了优化单线程的性能,将任务分成两种,一种是同步任务,另一种是异步任务。每次循环的迭代,都将中的回调函数加入任务队列等待执行。

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

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

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

甲:顺序打印0到9?

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

哪个答案正确?我们继续上图:

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

为什么会循环打印十个10而不是0到9?

从结果来看,for循环执行完跳出之后,才开始执行setTimeout(所以j才等于10),为什么不是每次迭代都执行一次setTimeout呢?

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

为什么会循环打印十个10

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

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

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

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

这就涉及到了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的定时,是定时插入执行栈之后立即执行,还是立即插入执行栈定时执行?

期待大家的留言。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107100.html

相关文章

  • 还担心面试官问闭包?

    摘要:一言以蔽之,闭包,你就得掌握。当函数记住并访问所在的词法作用域,闭包就产生了。所以闭包才会得以实现。从技术上讲,这就是闭包。执行后,他的内部作用域并不会消失,函数依然保持有作用域的闭包。 网上总结闭包的文章已经烂大街了,不敢说笔者这篇文章多么多么xxx,只是个人理解总结。各位看官瞅瞅就好,大神还希望多多指正。此篇文章总结与《JavaScript忍者秘籍》 《你不知道的JavaScri...

    tinyq 评论0 收藏0
  • 你不知道javascript》笔记_this

    下一篇:《你不知道的javascript》笔记_对象&原型 写在前面 上一篇博客我们知道词法作用域是由变量书写的位置决定的,那this又是在哪里确定的呢?如何能够精准的判断this的指向?这篇博客会逐条阐述 书中有这样几句话: this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式当一个函数被调用时...

    cpupro 评论0 收藏0
  • 【笔记】 你不知道JS读书笔记——异步

    摘要:异步请求线程在在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件循环队列中。 基础:浏览器 -- 多进程,每个tab页独立一个浏览器渲染进程(浏览器内核) 每个浏览器渲染进程是多线程的,主要包括:GUI渲染线程 JS引擎线程 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎) JS引擎线程负...

    junnplus 评论0 收藏0
  • JavaScript引擎是如何工作?从调用栈到Promise你需要知道一切

    摘要:最受欢迎的引擎是,在和中使用,用于,以及所使用的。怎么处理每个引擎都有一个基本组件,称为调用栈。也就是说,如果有其他函数等待执行,函数是不能离开调用栈的。每个异步函数在被送入调用栈之前必须通过回调队列。例如方法是在中传递的回调函数。   翻译:疯狂的技术宅 原文:www.valentinog.com/blog/engine… 从Call Stack,Global Me...

    zzbo 评论0 收藏0
  • 你不知道JavaScript :Promise 与 Async/Await

    摘要:前言对于这门语言,其实我更喜欢称它为,从一开始我们就已经涉及到异步编程,但是多数开发者从来没有认真思考过自己程序中的异步,到底是怎么实现的,以及为什么会出现。 前言 对于JavaScript这门语言,其实我更喜欢称它为ECMAScript,从一开始我们就已经涉及到异步编程,但是多数JavaScript开发者从来没有认真思考过自己程序中的异步,到底是怎么实现的,以及为什么会出现。但是由于...

    wmui 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<