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

相关推荐
dialect_5 小时前
KMP算法
开发语言·c++·算法
jktantan5 小时前
Vue keep-alive 切标签后滚动位置回顶?因为 onDeactivated 里读到的 scrollTop 是 0
前端·javascript·vue.js
总要学点什么5 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
2601_962885726 小时前
quotes 字段全解:涨跌幅、振幅、换手率的口径与单位
java·前端·spring
excel6 小时前
encodeURI 和 encodeURIComponent 有什么区别?
前端
前端精髓6 小时前
调用openrouter的免费API模型
java·前端·javascript
赛博守夜人7 小时前
AI安全攻防与治理之十四:AI 安全测试不能脱离传统安全,Web、API、移动端与模型攻击面的组合拳
前端·人工智能·安全
开开心心_Every7 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
chenbingjie_c7 小时前
C++ 多态完全指南:从虚函数重写到虚函数表原理
开发语言·c++
吴卫斌7 小时前
定投ETF实战指南(四):行业/主题ETF——定投的“进攻端“
开发语言·python·股票·量化