JS函数的执行时机

JS函数的执行时机

问题引入(五个5问题):

1
2
3
4
5
6
7
for(var i=0;i<5;i++){
setTimeout(()=>{
console.log(i)
},0)
}
//控制台打印五个5
//return为浏览器输出的,不关心。下文不再赘述。

为什么控制台打印出了五个5,而不是0、1、2、3、4,why

深入理解JS函数的执行时机

输出以上结果的前提是:

var声明使得变量提升。使用var命令声明,同时给未声明的变量赋值, 则执行赋值后, 该变量会被隐式地创建为全局变量(成为全局对象的属性)

因此,此代码中,for循环的i被提升为全局变量(for循环全程只有一个变量i)。

以上代码等价于:

1
2
3
4
let i
for(i=0;i<5;i++){
...
}

感觉这个代码比较啰嗦,以下均使用for循环内部的var命令来声明全局的i

② JavaScript是一门单线程语言。每一个任务只能按顺序依次进行。

上述代码中的window.setTimeout设置了一个定时器,该定时器在一段时期后(可以理解为之前的任务完成后)才执行一个函数或指定的一段代码。

因此在执行setTimeout(()=>{console.log(i);},0);时,for循环已经结束,此时i的值为5.

所以此时每次执行的console.log(i);结果都为5。

通俗的理解,在当前任务还没有完成时,就安排了下一个任务,而下一个任务的进行需要上一个任务得到的结果(这里前后两个任务分别是for循环和计时器)。那么JS会先把当前任务完成,然后再开始完成下一个任务。此时,下一个任务用到的数据应该是当前任务(for循环)完成之后(have done)的数据,而不是正在完成(v.ing)的数据。

异步任务

通常来说,程序都是顺序执行,同一时刻只会发生一件事。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户的角度来说,整个程序才算运行完毕。

使用异步函数可以设置函数队列的执行顺序。JS中最基本的异步函数有:

灵活使用(TL;DR)

如何打出想要的0-4?

理解关键点:只要 ①在局部作用域中创造它的局部变量i(方法一) 或者 ②在局部作用域使用全局变量i(方法二和方法三)就能够达到目的。

方法一(for循环内部使用let声明):

可以将var声明改为let声明。它能声明一个块级作用域的本地变量。

1
2
3
4
5
6
for(let i=0;i<5;i++){
setTimeout(()=>{
console.log(i);
},0);
}
//控制台打印0,1,2,3,4

let肯定不能放在for循环外面,上文已经解释。

此时,尽管计时器开始执行时,for循环已经结束,但每一次的i都是该次所在的块中的i,所以打印的是当时的i值。

注意,此时for循环将产生六个局部变量i,分别是0,1,2,3,4,5。

同样的道理,也可以在for循环中单独声明一个变量j(使用let或const)。此代码比较无聊,但对于理解方法一有帮助:

1
2
3
4
5
6
7
8
for(var i=0;i<5;i++){
let j=i
//const j=i
setTimeout(()=>{
console.log(j);
},0);
}
//控制台打印0,1,2,3,4

方法二(立即执行函数):

1
2
3
4
5
6
7
8
for(var i=0;i<5;i++){
!function(i){
setTimeout(()=>{
console.log(i)
},0)
}(i)
}
//控制台打印0,1,2,3,4

使用立即执行函数可以创造一个局部作用域,进而达到想要的效果:

1
2
3
4
5
!function(){
/*
局部作用域。
*/
}()

方法三(setTimeout函数传参):

window.setTimeout的第三个参数用于设定function的参数

1
2
3
4
5
6
for(var i=0;i<5;i++){
setTimeout(function(i){
console.log(i)
},0,i)
}
//控制台打印0,1,2,3,4

此时,每一次进入for循环,第三个参数都告诉计时器:不用等for循环完成,你现在就能拿到i值给你的function执行了。所以每一次都能直接打印出当次的i值。


版权声明:本文作者为「Andy8421」.本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!