为何setTimeout在for循环中表现异常?为何弹窗显示6而非5?
为什么setTimeout里的alert总是弹出6而不是5?
这是个经典的JavaScript异步机制+变量作用域问题,我来给你拆解明白~
首先先对比下两段代码的差异:
第一段同步执行的循环:
for (i = 1; i <= 5; i++) { alert(i) }
这里alert是同步操作,每一次循环迭代都会立刻执行,直接拿到当前i的数值,所以依次弹出1到5,完全符合预期。
而第二段带setTimeout的循环:
for (i = 1; i <= 5; i++) { setTimeout(function(){ alert(i) }); }
问题出在两个核心点上:
1. 异步回调的执行时机
setTimeout的回调函数不会立刻执行,它会被浏览器放到任务队列中,只有当当前所有的同步代码(也就是整个for循环)完全执行完毕后,才会依次执行队列里的回调函数。
2. 共享的变量引用
这里的i是全局变量(哪怕你用var声明,在全局环境下也是全局作用域),所有五个回调函数都共享同一个i的引用——它们并不是保存了循环时i的快照,而是在执行时才去读取i的当前值。
循环的完整执行流程
我们一步步看循环到底做了什么:
- 第1次迭代:
i=1,调用setTimeout把回调放进队列,然后执行i++,i变成2 - 第2次迭代:
i=2,放回调,i++变3 - ...
- 第5次迭代:
i=5,放回调,然后执行i++,i变成6 - 这时候检查循环条件
i<=5,6>5,循环终止
等循环结束后,浏览器才开始处理任务队列里的五个回调。这时候去读取i的值,它已经是6了,所以每个回调弹出的都是6。
那为什么不是5?因为循环的最后一步是:当i=5时,执行完循环体里的setTimeout后,会先执行i++让i变成6,再去判断循环条件是否成立——这时候条件不满足,循环才停止。所以循环结束时i的最终值是6,而非5。
怎么修复这个问题?
如果想让回调弹出1到5,有几种常用方法:
- 用
let声明i:利用ES6的块级作用域,每次循环迭代都会创建一个新的i变量,回调会绑定当前迭代的i:for (let i = 1; i <= 5; i++) { setTimeout(function(){ alert(i) }); } - 用立即执行函数(IIFE)创建闭包:把当前
i的值作为参数传入IIFE,保存为独立的变量:for (i = 1; i <= 5; i++) { (function(num) { setTimeout(function(){ alert(num) }); })(i) } - 用
setTimeout的第三个参数传值:直接把i作为参数传给回调:for (i = 1; i <= 5; i++) { setTimeout(function(num){ alert(num) }, 0, i) }
内容的提问来源于stack exchange,提问作者Dipesh rana
相关产品推荐
相关产品推荐

