JS函数执行的时机
一段代码
在讨论JS函数执行的时机之前,我们先来看一段代码
1 | let i = 0 |
请问,执行上述代码,最终控制台输出结果是什么?
- setTimeout的id, 0, 1, 2, 3, 4, 5
- setTimeout的id, 6, 6, 6, 6, 6, 6
可能有人会写出上述代码,并期望得到结果1,但实际控制台会输出结果2。
本篇文章,我会分析js的执行过程,来分析为什么会输出结果2,涉及到了调用栈,作用域与闭包,以及event loop与macro task queue,并在最后提出几种能够得到结果2的方法。
如果有小伙伴完全理解上述过程与结果,那么下面的内容其实可以不看,或者挑挑毛病。如果有小伙伴有任何疑问,也可以联系我,欢迎指教讨论。
JS的执行过程
下面我会用伪代码的形式来描述我所理解的js执行过程
分析过程较长,如已了解可略过
1 | let i = 0 // 全局执行上下文内声明一个变量,赋值为数字0 |
使用let关键字更改输出
这里的关键点,我认为有以下几点
首先是setTimeout内回调函数的执行时间,其次是setTimeout内回调函数使用的i变量的作用域
其实搞清i的作用域我们就知道,整个执行过程中,我们使用了一个i变量,并在最后输出6次i。
那么如果不更改setTimeout内的代码,有没有办法记录下i的值,并在之后输出呢,也就是实现控制台输出0, 1, 2, 3, 4, 5的效果呢
很显然,我们需要6个额外的变量来保存i每次循环时的值
在ES6之前,js并没有块级作用域,但在ES6新出的let
和const
中,使用这两个关键字会自动将其所在的{}变更为一个新的作用域。
而且为了记录for循环每次的i的值,js还额外做了一些工作,会将for循环的{}变成一个新的作用域。
此时我们改写代码如下
1 | for(let i = 0; i < 6; i++) { |
为了理解,我会将代码转化如下
1 | { |
此时在js执行完毕后,根据作用域链,查找到for循环的{}内,因为每次执行都创建新的作用域,所以每一轮循环都会有一个“新的i”,也就是类似于js保存了i变量此时的快照,所以总共有6个i,并且在for循环外是访问不到的,也就是setTimeout内的console.log(i)
携带了一个闭包。
关于闭包的定义及使用,如果有不清楚的小伙伴可以多查找一些资料。
这里也推荐一个系列视频
既然我们通过使用let
关键字,来实现效果,并且其核心原理就是增加新的作用域,那么其实我们也可以使用let
和const
来声明新变量的方式来实现同样的效果
1 | let i |
这里我使用了const
关键字来声明j变量,以便更明确的标明j变量是一个新的变量。
使用立即执行函数来创建作用域,更改输出
那么,如果不适用let
和const
关键字呢?
我们可以使用立即执行函数来创建作用域
1 | let i |
如果我们不想修改console.log(i)
,还可以写为如下
1 | let i |
此时每执行一次循环,就会将i的值作为参数传递给立即执行函数,console.log(i)
中i的值也就在进入macro task queue前就已确定。
以上就是我所想到的几种解决方案。
文章如有不对之处,希望看完的小伙伴批评指正。