[代码例题] 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

相关推荐
以卿a20 小时前
C++(继承)
开发语言·c++·算法
lly20240620 小时前
XQuery 选择和过滤
开发语言
码丁_11720 小时前
为什么前端需要做优化?
前端
测试工程师成长之路20 小时前
Serenity BDD 框架:Java + Selenium 全面指南(2026 最新)
java·开发语言·selenium
Mr Xu_20 小时前
告别硬编码:前端项目中配置驱动的实战优化指南
前端·javascript·数据结构
czxyvX20 小时前
017-AVL树(C++实现)
开发语言·数据结构·c++
你真是饿了20 小时前
1.C++入门基础
开发语言·c++
天天进步201520 小时前
Python全栈项目:实时数据处理平台
开发语言·python
Tipriest_20 小时前
Python中is关键字详细说明,比较的是地址还是值
开发语言·python
sheji341620 小时前
【开题答辩全过程】以 基于Python的餐饮统计系统的设计和实 现为例,包含答辩的问题和答案
开发语言·python