掘金小剧场:当setTimeout遇上循环,var和let的爱恨情仇!✨
嘿,各位前端的"程序猿"们,今天咱们来聊一个在面试中、工作中都可能让你挠头的小妖精------setTimeout
在循环里的那些"骚操作"!别看它平时人畜无害,一旦和for
循环手牵手,那可真是"一言不合就翻车"!
想象一下,你接到一个需求:每隔一秒打印出1、2、3、4。听起来是不是很简单?一个for
循环,里面套个setTimeout
,完美!然而,当你兴冲冲地写下代码,运行一看......咦?怎么打印出来的都是5?或者干脆啥也没打印?别急,这可不是你的代码出了bug,而是你和JavaScript的"异步"小脾气还没磨合好呢!
今天,就让我们一起揭开这个谜团,看看var
和let
这对"欢喜冤家"是如何在这场"定时打印"的战役中,各自施展绝技,最终抱得美人归(哦不,是正确打印出结果)的!准备好了吗?发车!🚀
🚨 踩坑现场:为什么都是5?
首先,我们来看看那个让你"一脸懵逼"的场景。当你用var
来声明循环变量i
时,代码大概长这样:
javascript
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
你满心期待它能每隔一秒打印出 0, 1, 2, 3, 4
。结果呢?它可能一口气打印出五个 5
!或者,如果你把setTimeout
的延迟时间设得特别短,你可能会看到几乎同时打印出五个 5
。
这是为什么呢?🤔
答案就在于var
的"脾气"------它没有块级作用域!当for
循环飞速执行的时候,setTimeout
里面的回调函数并没有立即执行,它们被扔进了任务队列,等待主线程空闲下来,并且等待设定的延迟时间到达。而此时,for
循环早就跑完了,i
的值已经变成了最终的5
。所以,当那些回调函数终于被执行的时候,它们引用的i
,都是同一个i
,也就是那个最终的5
。
这就好比你给五个小弟发了任务,让他们一秒后去拿一个包裹。结果你只告诉他们去"拿包裹",没告诉他们拿哪个包裹。等一秒后,包裹已经被你拿走了,他们只能拿到你最后拿走的那个包裹(也就是i
的最终值)。是不是有点"坑"?
🔄 闭包大法:给每个i一个"专属空间"
为了解决var
带来的作用域问题,我们可以请出JavaScript的"老朋友"------闭包!闭包就像一个"私人订制"的盒子,能把每次循环的i
值都好好地"装"起来,不让它被后续的循环修改。
看看图片中给出的闭包解决方案:
javascript
// 使用闭包实现
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
这里,我们用了一个立即执行函数表达式(IIFE)。每次循环,i
的值都会作为参数传递给这个IIFE。IIFE会立即执行,并且在自己的作用域内创建一个新的i
的副本。setTimeout
里面的回调函数引用的是这个IIFE内部的i
,而不是外部循环的i
。这样,每个回调函数都拥有了自己独立的i
值,完美解决了问题!
这就好比你给五个小弟发任务,这次你聪明了,你把每个包裹都分别编号,然后告诉每个小弟去拿对应编号的包裹。这样,即使你把所有包裹都拿走了,小弟们也知道自己该拿哪个,是不是很机智?
🔧 let的魔法:块级作用域的魅力
ES6(ECMAScript 2015)引入了let
和const
,它们带来了"块级作用域"这个强大的特性,彻底解决了var
在循环中遇到的"坑"。
再来看看图片中给出的let
解决方案:
javascript
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
是不是感觉代码瞬间清爽了许多?仅仅把var
换成let
,问题就迎刃而解了!
这是因为let
声明的变量具有块级作用域。在for
循环中,每次迭代都会为let i
创建一个新的绑定。这意味着,每次循环的i
都是一个全新的变量,它只存在于当前循环的这个"块"中。当setTimeout
的回调函数被创建时,它会"记住"当前迭代中i
的值,而不是等到循环结束后的最终值。
这就像你给五个小弟发任务,这次你直接给每个小弟发了一个"专属任务卡",上面清清楚楚地写着他们要拿的包裹编号。每个小弟的任务卡都是独立的,互不影响。所以,无论你什么时候去执行这些任务,他们都能准确无误地拿到自己对应的包裹。let
就是这么贴心,这么省心!
💡 总结:var vs let,谁是你的菜?
通过上面的分析,我们可以清晰地看到var
和let
在处理循环中的异步操作时,表现出了截然不同的"性格":
特性/方案 | var (传统艺能) |
let (ES6新贵) |
闭包 (老牌解决方案) |
---|---|---|---|
作用域 | 函数作用域或全局作用域 | 块级作用域 | 通过函数创建独立作用域 |
变量提升 | 有 | 无 | 无 |
循环中的表现 | 变量共享,导致异步操作引用到最终值 | 每次循环创建新变量,异步操作引用到当前值 | 为每次循环创建独立作用域,保存当前值 |
代码简洁性 | 简洁(但易出错) | 极其简洁,推荐使用 | 相对复杂,需要额外函数包裹 |
适用场景 | 早期JS代码,或不需要独立作用域的场景 | 现代JS开发,解决循环异步问题首选 | 兼容旧环境,或需要更精细作用域控制的场景 |
简单来说:
- 如果你还在用
var
,并且遇到了循环异步的问题,那么闭包是一个非常有效的解决方案,它能帮你"手动"创建出独立的作用域。但代码会稍微复杂一些。 - 如果你已经拥抱了ES6,那么恭喜你,
let
就是你的"救星"!它以最简洁的方式,优雅地解决了这个问题,让你的代码更加清晰、不易出错。
所以,在现代JavaScript开发中,我们强烈推荐使用let
来声明循环变量,它能让你少掉很多头发,多写几行优雅的代码!
🔍 let的块级作用域:图解魔法
为了更好地理解let
为什么能解决这个问题,我们可以看看let
的块级作用域是如何工作的。每次循环,let
都会创建一个新的变量绑定,确保每个setTimeout
回调函数都能捕获到独立的i
值。

从图中可以看出,let
声明的变量只在当前代码块内有效,这使得每次循环的i
都是独立的。
⏳ setTimeout与循环:异步的舞蹈
setTimeout
是一个异步函数,它不会阻塞主线程的执行。当for
循环快速执行时,setTimeout
的回调函数会被放入任务队列,等待主线程空闲时再执行。理解这一点对于理解var
的问题至关重要。

这张图展示了setTimeout
在循环中的执行流程,有助于我们理解为什么var
会导致所有回调函数都引用同一个最终值。
🌀 事件循环:异步的幕后英雄
要彻底理解JavaScript的异步机制,就不得不提"事件循环"(Event Loop)。setTimeout
的回调函数正是通过事件循环机制才得以在主线程空闲时执行。

事件循环是JavaScript实现非阻塞I/O的关键。它不断地检查任务队列,并将准备好的任务推到调用栈中执行。理解事件循环,能帮助我们更好地掌握JavaScript的异步编程。
🎉 结语:拥抱ES6,告别踩坑!
通过今天的"掘金小剧场",相信你对setTimeout
在循环中的表现,以及var
、let
和闭包在其中的作用,有了更深入的理解。在日常开发中,遇到类似问题时,请优先考虑使用let
,它会让你事半功倍!
当然,JavaScript的异步世界远不止这些,事件循环(Event Loop)、Promise、async/await等等,都是值得我们深入探索的宝藏。保持好奇心,持续学习,你就能成为JavaScript的"驯兽师",驾驭各种"异步猛兽"!
如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、转发哦!也欢迎在评论区分享你的看法和遇到的趣事,我们一起交流,共同进步!下期"掘金小剧场",我们不见不散!👋