var in for loop.
现在是2025年,ES6已经发布快10年了,除了一些基础前端框架外,在常规的业务代码中,我们不应该再使用var了,因为 var有很多的问题:比如变量提升,比如没有块级作用域。我们从一道面试题来分析var的缺点。
1 | for (var i = 0; i < 5; i++) { |
Answer:
1 | 100 |
分析:
- 最后一行代码
console.log(100);是同步代码,所以会先输出100。 - for循环一共执行5次,每次都会调用
setTimeout,但是setTimeout是异步代码,JS引擎会将setTimeout的回调函数放到宏任务队列中,等待执行。 - 当前Event loop执行完毕后,会去执行宏任务队列中的任务,这时候
setTimeout的回调函数才会被执行,但是这时候i已经变成了5,所以会输出5个5。
所以导致这段代码的问题有二,解决其中任何一个都可以让它输出0, 1, 2, 3, 4。
- 使用了
var,var是函数作用域,所以所有的循环都共享一个i。 - 使用了
setTimeout。setTimeout是整个循环结束后才开始执行的!如果我们把setTimeout删除就不会有问题了。以下代码works well!1
2
3for (var i = 0; i < 5; i++) {
console.log(i);
}
那么是不是不用setTimeout就没有问题了呢?不是的,我们来看这道题的一个变种:
1 | const func = []; |
这个变种没有使用setTimeout,但是结果依然是5个5。为什么呢?观察这个变种和原始题目我们可以看到,他们共同的特点都是在for循环结束后才执行代码,此时i的值已经是5了。这才是根本原因。
好了,现在我们来看看如何解决这个问题。
方法一,使用let代替var。
由于let有块级作用域,所以每次循环都会创建一个新的变量i,而不是像var那样所有的循环都共享一个i。
1 | for (let i = 0; i < 5; i++) { |
方法二:将循环变量i参数化
说得再通俗一点,将setTimeout的逻辑单独抽离成一个函数print。这样每次循环的时候,都会将当前的i传递给print函数,这样setTimeout的代码加入宏任务队列时会记住当前的i值。
1 | for (var i = 0; i < 5; i++) { |
如果觉得单独抽离函数不够优雅,可以使用IIFE(立即执行函数表达式)。注意,这里和闭包没有半毛钱关系,IIFE这里只是为了传递参数,也就是上面抽离函数的简化版。
1 | for (var i = 0; i < 5; i++) { |
方法三:使用setTimeout的第三个参数
这个方法可能很多同学都没有见过,其实setTimeout方法是有第三个参数的,这个参数是用来传递给回调函数的参数的。所以我们可以将当前的i值传递给setTimeout的回调函数。
1 | for (var i = 0; i < 5; i++) { |
为了做区分,我把回到函数中的i改成了j,这样就不会混淆了。循环变量i通过setTimeout的第三个参数传递给回调函数。
你学会了吗?