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

相关推荐
We་ct11 小时前
LeetCode 5. 最长回文子串:DP + 中心扩展
前端·javascript·算法·leetcode·typescript
JAVA面经实录91714 小时前
Java企业级工程化·终极完整版背诵手册(无遗漏、全覆盖、面试+落地通用)
java·开发语言·面试
陈随易15 小时前
有生之年系列,Nodejs进程管理pm2 v7.0发布
前端·后端·程序员
冰暮流星15 小时前
javascript之事件代理/事件委托
前端
周杰伦fans15 小时前
AutoCAD .NET 二次开发:深入理解 EntityJig 的工作原理与正确实现
开发语言·.net
陈随易16 小时前
AI时代,你还在坚持手搓文章吗
前端·后端·程序员
Bat U17 小时前
JavaEE|多线程初阶(七)
java·开发语言
谭欣辰17 小时前
C++ 排列组合完整指南
开发语言·c++·算法
foundbug99918 小时前
自适应滤除直达波干扰的MATLAB实现
开发语言·算法·matlab