你不知道的JavaScript -- setTimeout
谈起setTimeout,就让我想起了一道经典的前端面试题
for(var i = 1; i <= 5; i ++) {
setTimeout(function time() {
console.log(i);
}, 1000);
}
看到这几行代码,看过类似题目的大家肯定都知道结果是输出5个6
,
这是由于i变量声明在全局,导致只有一个全局作用域里面只有一个i变量,所以i变量的变化值没有地方存储。
解决办法大家估计也清楚,简单的介绍2种:
1、立即执行函数
for(var i = 1; i <= 5; i ++) {
(function (j) {
setTimeout(function time() {
console.log(j);
}, 1000);
})(i)
}
2、块级作用域
for(let i = 1; i <= 5; i ++) {
setTimeout(function time() {
console.log(i);
}, 1000);
}
为什么会输出5个6呢?
前面说的只是很简单的一方面原因,真正涉及的核心是javaScript的单线程特性。
JavaScript设计的初衷,是浏览器用来与用户进行交互和DOM操作的。这就决定了它必须是单线程的,设想JavaScript同事有两个线程,一个线程在DOM节点添加内容,一个线程删除该节点,浏览器就会出现混乱。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
为了优化单线程的性能,JavaScript将任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有主线程中的同步任务执行完毕,异步任务才会进入执行队列执行。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
这样我们就可以很好地理解为什么会输出5个6
了,由于setTimeout
的执行是异步任务,所以放在任务队列中等待执行,for
循环执行完之后,全局作用域的变量i
此时已经变成了6,异步任务开始执行,去获取变量i
只能得到6,所以最后输出的结果就是5个6了。
如果想方便理解的话,把过程简化成等效的同步代码如下:
for(var i = 1; i <= 5; i ++) {}
setTimeout(function time() { console.log(i);}, 1000); // 6
setTimeout(function time() { console.log(i);}, 1000); // 6
setTimeout(function time() { console.log(i);}, 1000); // 6
setTimeout(function time() { console.log(i);}, 1000); // 6
setTimeout(function time() { console.log(i);}, 1000); // 6
最后给一个简单题目看看结果是什么?
setTimeout(function(){while(true){ console.log(0)}},1300);
setTimeout(function(){console.log(1)},1600);
setTimeout(function(){console.log(2)},1000);
是不是想着最开始输出2,0,1呢?
其实结果不是输出2,0,1。
控制台执行下,那么结果是2,0,0......,一个完美的死循环。
所以别只关注定时器的问题,忽略了while语句了。