TL;DR
- setTimeout()的倒计时是在所有可执行代码执行完毕后才开始。
- 多个setTimeout()各自独立进行倒计时。
- 实现for-setTimeout情况下依次输出i的主要方法有:for-let-setTimeout、闭包、拆分结构。
为什么是6 6 6 6 6 6,而不是0 1 2 3 4 5?
首先来看一段代码:
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
一段很简单的let-for-setTimeout代码,setTimeout()内执行代码内容为简单的console.log()函数。
但对于新人来说,无法理解的是为什这段代码在控制台的打印结果为6 6 6 6 6 6。
关于这点,我们粗略的可以细分为两个小问题:
- 为什么是
6 6 6 6 6 6,而不是0 1 2 3 4 5 - 为什么是
6 6 6 6 6 6,而不是5 5 5 5 5 5
setTimeout()的机制
对于这两个问题,我们首先要理解的是setTimeout()的运行机制,它的语法格式如下:
setTimeout(function[,delay])
function是在倒计时结束后所要执行的函数。
delay是延迟,即倒计时的毫秒数,一个可选项。默认 0,意为“马上”执行。
新人所理解的setTimeout(),是在代码执行到setTimeout(()=>{console.log(i)},0)一段后,立即开始计时,在倒计时结束后立即开始执行setTimeout()内的函数。以如下代码为例:
let i = 0
setTimeout(function(){
console.log(i)
}, 1)
i = 1
在一部分JS新手眼中,这段代码的执行过程是这样的:
但实际上,setTimeout()的计时是在所有可执行代码执行完毕后才开始,无论给的延迟参数有多小,哪怕是0。
因此,对于刚给出的代码,JS中的执行过程其实是这样的:
对于这点,用如下这段代码可能更加体现得更加明显:
setTimeout(()=>console.log(1), 5000) // 5s
console.log(2)
控制台会先打印2,并停顿约5s的时间之后,再打印出1。
多个setTimeout()的情况
由此,我们延伸出一个问题:在多个setTimeout()的情况下,又会如何呢?我们来执行如下代码:
setTimeout(()=>console.log(1), 5000) // 5s
setTimeout(()=>console.log(2), 5000) // 5s
console.log(3)
通过粗略的秒表计时(你可以用你手机的内置计时功能),我们可以明显感觉到,代码执行后几乎是立即控制台就打印了3,在大约5秒后,依次打印了1、2。
setTimeout(()=>console.log(1), 10000) // 10s
setTimeout(()=>console.log(2), 5000) // 5s
console.log(3)
遵循之前的操作, 我们可以明显感觉到,控制台立即打印了3,大约5秒后打印了2,大约10秒后打印了1。
由此我们可以得出结论,多个setTimeout()的情况下,并在可执行代码执行完毕后同时开始倒计时,每个setTimeout()计时结束后立即开始执行。仅有当延迟相同时,才会按照源代码中的先后顺序来依次执行。
为什么会打印6 6 6 6 6 6?
回到本篇文章最开始的代码:
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
为了直观,我们可以把它转化成以下等价的的代码:
let i = 0
setTimeout(()=>{console.log(i)},0)
i++ // for循环:先执行判断、再执行循环体、最后执行i++
setTimeout(()=>{console.log(i)},0)
i++
setTimeout(()=>{console.log(i)},0)
i++
setTimeout(()=>{console.log(i)},0)
i++
setTimeout(()=>{console.log(i)},0)
i++
setTimeout(()=>{console.log(i)},0)
i++
通过之前的分析,我们可以很轻易的分析出,这段代码会先执行6次setTimeout()和i++。待所有可以执行的代码执行完毕后,再开始执行setTimeout()内部的函数,即()=>{console.log(i)}。回到之前提出的两个问题:
- 为什么是
6 6 6 6 6 6,而不是0 1 2 3 4 5 - 为什么是
6 6 6 6 6 6,而不是5 5 5 5 5 5
对于问题1,其原因在于setTimeout()的运行机制:它不是执行到setTimeout()后立即开始倒计时的,而是在所有可执行代码执行完毕后才开始倒计时的。或者说我们可以这么理解,setTimeout()的功能在于:给出一个在所有当前可执行代码结束后才开始的倒计时,在倒计时结束后,往代码末增加一段新的代码。
()=>{console.log(i)}是在执行6次i++后,才开始执行的。
对于问题2,其原因在for循环的执行顺序:先执行判断、再执行循环体、最后执行i++。即在第6次循环体执行完毕后,还会再执行一次i++,使i从5变成6。
如何打印0 1 2 3 4 5?
那么,我们如何才能使用for循环配合setTimeout()来打印出0 1 2 3 4 5的效果呢?
for-let
JS通过为for-let设置了特殊机制,为我们实现了这个需求:
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
// 0 1 2 3 4 5
通过for-let-setTimeout语序下,可以实现依次打印i的不同值的功能。为了区别,我们本文开始的代码:
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
// 6 6 6 6 6 6
我们可以看出,这两段代码最大的区别在于let的位置。我将后者称为let-for-setTimeout语序。
在前者,即for-let-setTimeout语序下,JS会特别地在每次i++的过程中新创建一个i,而每次console.log(i)所访问的,都是不同的i,我们可以这样理解它:
let i
for(i = 0; i<6; i++){
let j = i
setTimeout(()=>{
console.log(j)
},0)
}
// 0 1 2 3 4 5
闭包
通过闭包,将i的变量驻留在内存中,这种情况下在执行setTimeout()时,就已经确定输出了。
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout(function(){
console.log( j );
},0);
})(i);
}
拆分结构
将计时器函数定义在循环外部,这样每次循环都会将当前的i作为实参传递给外部的函数fn()。
function fn(i) {
setTimeout(() => console.log(i), 0);
}
for (var i = 1; i <= 5; i++) {
fn(i);
}
关于setTimeout()的第三个参数
for (var i = 1; i <= 5; i++) {
setTimeout(function(){
console.log(i);
}, 0, i);
}
很多网站都将它列为for-setTimeout情况下依次输出i的方法之一,但实际似乎并不是如此。很多代码之所以能依次输出i,大概是因为他们使用了let定义。
通过查阅MDN,我们也可以得到对于setTimeout()第三个参数的描述:
附加参数,一旦定时器到期,它们会作为参数传递给function
显然,哪怕给予其第三个参数,参数也是在定时器到期后才传递给setTimeout()的。