[代码例题] var 和 let 在循环中的作用域差异,以及闭包和事件循环的影响

复制代码
for(var a = 0; a < 4; a++){
  setTimeout(() => {
    console.log(a)  
  }, 1000)
}
for(let a = 0; a < 4; a++){
  setTimeout(() => {
    console.log(a) 
  }, 1000)
}

这段代码演示了 var 和 let 在循环中的作用域差异 ,以及闭包事件循环的影响。让我们逐步分析:

代码分析

第一个循环:使用 var

复制代码
for(var a = 0; a < 4; a++){
  setTimeout(() => {
    console.log(a)  
  }, 1000)
}

运行结果:打印4次 4

原因分析:

  1. var 的作用域var 是函数作用域(或全局作用域),这里的 a 在循环结束后仍然存在

  2. 闭包问题 :所有 setTimeout 回调函数都引用了同一个变量 a

  3. 时间差 :当1秒后回调函数执行时,循环早已结束,此时 a 的值是 4

  4. 执行顺序

    • 循环快速执行完,创建了4个定时器

    • 此时 a 的值已经是 4

    • 1秒后,4个回调函数执行,都打印当前 a 的值(4)

第二个循环:使用 let

复制代码
for(let a = 0; a < 4; a++){
  setTimeout(() => {
    console.log(a) 
  }, 1000)
}

运行结果:打印 0, 1, 2, 3

原因分析:

  1. let 的作用域let 是块级作用域,每次循环都会创建一个新的 a

  2. 闭包捕获 :每个 setTimeout 回调函数都捕获了各自循环迭代时的 a

  3. 独立变量 :实际上有4个不同的 a 变量,值分别是 0, 1, 2, 3

底层原理

使用 var 的等价代码

复制代码
// 实际执行类似这样
var a;  // 变量提升

a = 0;
setTimeout(() => { console.log(a); }, 1000);
a = 1;
setTimeout(() => { console.log(a); }, 1000);
a = 2;
setTimeout(() => { console.log(a); }, 1000);
a = 3;
setTimeout(() => { console.log(a); }, 1000);
a = 4;  // 循环结束条件

// 1秒后:所有回调都打印 4

使用 let 的等价代码

复制代码
// 实际执行类似这样
{
  let a = 0;
  setTimeout(() => { console.log(a); }, 1000);
}
{
  let a = 1;
  setTimeout(() => { console.log(a); }, 1000);
}
{
  let a = 2;
  setTimeout(() => { console.log(a); }, 1000);
}
{
  let a = 3;
  setTimeout(() => { console.log(a); }, 1000);
}
// 每个块有自己的 a,互不干扰

如何让 var 也正确输出?

方法1:使用闭包(IIFE)

复制代码
for(var a = 0; a < 4; a++){
  (function(i){
    setTimeout(() => {
      console.log(i)
    }, 1000)
  })(a)
}
// 输出: 0, 1, 2, 3

方法2:利用 setTimeout 的第三个参数

复制代码
for(var a = 0; a < 4; a++){
  setTimeout((i) => {
    console.log(i)
  }, 1000, a)  // a作为参数传入
}
// 输出: 0, 1, 2, 3

方法3:使用 forEach

复制代码
[0, 1, 2, 3].forEach(a => {
  setTimeout(() => {
    console.log(a)
  }, 1000)
})
// 输出: 0, 1, 2, 3

时间相关的重要说明

复制代码
// 这个代码的输出时间很有趣
console.log('开始');

for(let i = 0; i < 4; i++){
  setTimeout(() => {
    console.log(i, '在', Date.now(), '执行');
  }, 1000)
}

console.log('结束');

// 输出顺序:
// 开始
// 结束
// (大约1秒后)
// 0 在 [时间戳] 执行
// 1 在 [时间戳] 执行  
// 2 在 [时间戳] 执行
// 3 在 [时间戳] 执行
// 注意:四个几乎是同时执行的,不是间隔1秒!

实际应用场景

场景1:循环绑定事件

复制代码
// ❌ 错误做法 - 所有按钮都打印 3
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
  buttons[i].addEventListener('click', () => {
    console.log('点击了按钮', i);  // 总是最后一个索引
  });
}

// ✅ 正确做法
for(let i = 0; i < buttons.length; i++){
  buttons[i].addEventListener('click', () => {
    console.log('点击了按钮', i);  // 正确的索引
  });
}

场景2:异步请求

复制代码
// 使用 var 的问题
for(var i = 0; i < 3; i++){
  fetch('/api/data')
    .then(() => console.log('完成', i));  // 都打印 3
}

// 使用 let 解决
for(let i = 0; i < 3; i++){
  fetch('/api/data')
    .then(() => console.log('完成', i));  // 0, 1, 2
}

总结

特性 var let
作用域 函数/全局作用域 块级作用域
变量提升 否(暂时性死区)
循环中 共享同一个变量 每次迭代创建新变量
闭包问题 需要额外处理 自动捕获迭代值
推荐程度 ❌ 现代开发避免 ✅ 推荐使用

关键点:

  1. var 在循环中会创建闭包问题

  2. let 为每次循环创建新的词法环境

  3. 理解这个区别对处理异步编程至关重要

  4. 现代JavaScript开发应该优先使用 let/const

相关推荐
HashTang2 小时前
【AI 编程实战】第 3 篇:后端小白也能写 API:AI 带我 1 小时搭完 Next.js 服务
前端·后端·ai编程
三年三月2 小时前
React 中 CSS Modules 详解
前端·css
JANG10242 小时前
【Linux】常用指令
linux·服务器·javascript
粉末的沉淀2 小时前
tauri:关闭窗口后最小化到托盘
前端·javascript·vue.js
weixin_462446232 小时前
EasyExcel 动态修改模板 Sheet 名称:自定义 SheetWriteHandler 拦截器
java·开发语言·easyexcel
赵庆明老师2 小时前
NET 使用SmtpClient 发送邮件
java·服务器·前端
绝世唐门三哥3 小时前
使用Intersection Observer js实现超出视口固定底部按钮
开发语言·前端·javascript
南山安3 小时前
Vue学习:ref响应式数据、v-指令、computed
javascript·vue.js·面试
Ayu阿予3 小时前
C++从源文件到可执行文件的过程
开发语言·c++