对for(var i = 0;i < 5;i++) {setTimeout(() => console.log(i),1000)}的深入分析

一个常见的问题

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是异步代码。

原来如此,我们立刻联想到事件循环,事件循环可以帮助我们区分同步代码和异步代码的执行时机。事件循环简单来讲是先执行宏任务,再清空微任务队列的一个循环。
其中常见的宏任务有

  1. <script>标签下所有同步代码
  2. setTimeout、setInterval
  3. I/O
  4. UI交互

常见的微任务有

  1. 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。

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值