你不知道的Javascript:有趣的setTimeout
今天在回顾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的定时,是定时插入执行栈之后立即执行,还是立即插入执行栈定时执行?
期待大家的留言。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Transform和Task之间有关?| Gradle
- user.ini文件构成的PHP后门
- Android厂商推送Plugin化 | 掘金技术征文-双节特别篇
- 关于lnmp目录禁止执行的绕过与正确方法
- Quill编辑器自定义字体和字体大小
- emlog某重要插件前台SQL注入+Getshell
- ES6中的对象与类
- QQ某业务主站DOM XSS挖掘与分析(绕过WAF)
- 品优购电梯导航案例
- stm32mp157开发板LCD 显示测试&触摸屏测试
- stm32mp157开发板LINE 接口测试方法
- stm32mp157开发板MIC 接口测试方法
- stm32mp157开发板声卡接口测试
- stm32mp157开发板USB Host 接口&OTG 接口测试
- Centos7安装ovs