当前位置:网站首页>对for(var i = 0;i < 5;i++) {setTimeout(() => console.log(i),1000)}的深入分析
对for(var i = 0;i < 5;i++) {setTimeout(() => console.log(i),1000)}的深入分析
2022-07-05 05:47:00 【weixin_41387874】
一个常见的问题
for(var i = 0;i < 5;i++)
{
setTimeout(() => console.log(i),1000);
}
对于这个问题,想必大家都有所耳闻,最终输出是
5 5 5 5 5
毫无疑问,这是和我们的预期输出不符的(预期输出是0 1 2 3 4)。造成这个问题的原因,就是var关键字声明的标识符的作用域范围是函数作用域。
因此在上面的js代码中,i标识符是存放在全局作用域中的。因此,当setTimeout的回调函数执行的时候,i标识符存放的值已经是5了(5是循环结束的终点),因此将输出5个5。
但这里还有一个疑点,为什么setTimeout执行的时候i已经是5了,是因为等待了1秒钟嘛?
setTimeout的执行时机
现在我们知道了第一个问题的答案和造成错误的原因,那么下面这个问题呢
for(var i = 0;i < 5;i++)
{
setTimeout(() => console.log(i),0);
}
实际上这个问题的输出也是
5 5 5 5 5
这个输出实际上告诉了我们刚刚疑点的答案,并不是因为等待了1秒钟导致setTimeout执行的时候i中存放的值变成了5。
那么是什么原因呢?
我们可以仔细阅读一下这个代码,我们发现for循环是同步代码,而setTimeout是异步代码。
原来如此,我们立刻联想到事件循环,事件循环可以帮助我们区分同步代码和异步代码的执行时机。事件循环简单来讲是先执行宏任务,再清空微任务队列的一个循环。
其中常见的宏任务有
- <script>标签下所有同步代码
- setTimeout、setInterval
- I/O
- UI交互
常见的微任务有
- Promise相关方法
因此js引擎在执行这个for循环的时候,遇到了setTimeout,将会将setTimeout放入宏任务队列,之后接着执行同步代码,当同步代码执行完毕后去检查微任务队列,在下一轮事件循环开始的时候才会将setTimeout从宏任务队列中取出。
因此,setTimeout的执行必然在所有同步代码,也就是整个for循环执行完毕之后,因此在setTimeout执行的时候,i已经是5了。
解决方法
现在我们明确了造成代码输出和预期不符的原因,也就是作用域。
首先很容易想到,如果把标识符i限制在块作用域内,就可以解决问题。
使用let
for(let i = 0;i < 5;i++)
{
setTimeout(() => console.log(i),1000);
}
setTimeout的回调函数() => console.log(i)的词法作用域是for循环内部的块作用域。而这个函数被指定为回调函数,毫无疑问,将会在其词法作用域以外被调用,并且当它被调用的时候js引擎会对i进行RHS引用,因此即使for循环已经执行完毕,其每一层循环的作用域依旧被() => console.log(i)维持,因此setTimeout在执行的时候可以访问到每一层循环的i。这实际上形成了我们常说的闭包(一个函数在其被定义的词法作用域以外被调用,并且维持着对其被定义的词法作用域中变量的引用)。
使用IIFE
IIFE是Immediately Invoked Function Expression,立即调用函数表达式。
for(var i = 0;i < 5;i++)
{
setTimeout((function(i){
return () => console.log(i);
})(i),1000)
}
这里使用IIFE的目的是使用闭包。每一层循环执行IIFE相当于为每一层循环都创建了一个函数作用域,并把每一层循环的i的值作为参数传入,存放在IIFE的函数作用域的同名标识符中。并且这个IIFE将() => console.log(i)作为值返回给了setTimeout。
同样的,() => console.log(i)被定义在IIFE的函数作用域,它将在它所在的词法作用域以外被调用,并且在() => console.log(i)中维持了对它所在词法作用域中的变量的引用(在这里,实际上词法作用域的作用域链查找中的‘同名遮蔽‘也起了作用)。() => console.log(i)函数在调用时,js引擎会对i进行RHS查找,js引擎首先询问了() => console.log(i)的自身函数作用域,发现并没有这个标识符,于是根据词法作用域查找规则,去() => console.log(i)的上一级词法作用域,也就是IIFE的函数作用域查找标识符i,在这里js引擎发现了标识符i,因此有这个闭包存在(对IIFE作用域的引用),setTimeout在执行的时候可以访问到每一层循环的i。
边栏推荐
- Some common problems in the assessment of network engineers: WLAN, BGP, switch
- 读者写者模型
- Brief introduction to tcp/ip protocol stack
- LeetCode 0107.二叉树的层序遍历II - 另一种方法
- QT判断界面当前点击的按钮和当前鼠标坐标
- AtCoder Grand Contest 013 E - Placing Squares
- 2022年貴州省職業院校技能大賽中職組網絡安全賽項規程
- PC register
- Dynamic planning solution ideas and summary (30000 words)
- High precision subtraction
猜你喜欢
Dynamic planning solution ideas and summary (30000 words)
R language [import and export of dataset]
Implement an iterative stack
Personal developed penetration testing tool Satania v1.2 update
YOLOv5-Shufflenetv2
剑指 Offer 53 - II. 0~n-1中缺失的数字
Solution to the palindrome string (Luogu p5041 haoi2009)
挂起等待锁 vs 自旋锁(两者的使用场合)
wordpress切换页面,域名变回了IP地址
On the characteristics of technology entrepreneurs from Dijkstra's Turing Award speech
随机推荐
Light a light with stm32
Educational codeforces round 109 (rated for Div. 2) C. robot collisions D. armchairs
Sword finger offer 05 Replace spaces
【Jailhouse 文章】Jailhouse Hypervisor
Sword finger offer 58 - ii Rotate string left
Brief introduction to tcp/ip protocol stack
Introduction and experience of wazuh open source host security solution
1.14 - 流水线
API related to TCP connection
Implement a fixed capacity stack
Some common problems in the assessment of network engineers: WLAN, BGP, switch
One question per day 1447 Simplest fraction
[jailhouse article] performance measurements for hypervisors on embedded ARM processors
剑指 Offer 04. 二维数组中的查找
挂起等待锁 vs 自旋锁(两者的使用场合)
Collection: programming related websites and books
Csp-j-2020-excellent split multiple solutions
[jailhouse article] jailhouse hypervisor
Using HashMap to realize simple cache
剑指 Offer 09. 用两个栈实现队列