图解 setTimeout + 循环:var 共享变量 vs let 独立绑定

掘金小剧场:当setTimeout遇上循环,var和let的爱恨情仇!✨

嘿,各位前端的"程序猿"们,今天咱们来聊一个在面试中、工作中都可能让你挠头的小妖精------setTimeout在循环里的那些"骚操作"!别看它平时人畜无害,一旦和for循环手牵手,那可真是"一言不合就翻车"!

想象一下,你接到一个需求:每隔一秒打印出1、2、3、4。听起来是不是很简单?一个for循环,里面套个setTimeout,完美!然而,当你兴冲冲地写下代码,运行一看......咦?怎么打印出来的都是5?或者干脆啥也没打印?别急,这可不是你的代码出了bug,而是你和JavaScript的"异步"小脾气还没磨合好呢!

今天,就让我们一起揭开这个谜团,看看varlet这对"欢喜冤家"是如何在这场"定时打印"的战役中,各自施展绝技,最终抱得美人归(哦不,是正确打印出结果)的!准备好了吗?发车!🚀

🚨 踩坑现场:为什么都是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)引入了letconst,它们带来了"块级作用域"这个强大的特性,彻底解决了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,谁是你的菜?

通过上面的分析,我们可以清晰地看到varlet在处理循环中的异步操作时,表现出了截然不同的"性格":

特性/方案 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在循环中的表现,以及varlet和闭包在其中的作用,有了更深入的理解。在日常开发中,遇到类似问题时,请优先考虑使用let,它会让你事半功倍!

当然,JavaScript的异步世界远不止这些,事件循环(Event Loop)、Promise、async/await等等,都是值得我们深入探索的宝藏。保持好奇心,持续学习,你就能成为JavaScript的"驯兽师",驾驭各种"异步猛兽"!

如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、转发哦!也欢迎在评论区分享你的看法和遇到的趣事,我们一起交流,共同进步!下期"掘金小剧场",我们不见不散!👋

相关推荐
Danny_FD19 分钟前
Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例
前端
uhakadotcom20 分钟前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试
韦小勇21 分钟前
el-table 父子数据层级嵌套表格
前端
奔赴_向往23 分钟前
为什么 PWA 至今没能「掘进」主流?
前端
小小愿望23 分钟前
微信小程序开发实战:图片转 Base64 全解析
前端·微信小程序
掘金安东尼25 分钟前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
电商API大数据接口开发Cris25 分钟前
基于 Flink 的淘宝实时数据管道设计:商品详情流式处理与异构存储
前端·数据挖掘·api
小小愿望27 分钟前
解锁前端新技能:让JavaScript与CSS变量共舞
前端·javascript·css
程序员鱼皮30 分钟前
爆肝2月,我的 AI 代码生成平台上线了!
java·前端·编程·软件开发·项目
天生我材必有用_吴用43 分钟前
一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势
前端·vue.js