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
原因分析:
-
var 的作用域 :
var是函数作用域(或全局作用域),这里的a在循环结束后仍然存在 -
闭包问题 :所有
setTimeout回调函数都引用了同一个变量a -
时间差 :当1秒后回调函数执行时,循环早已结束,此时
a的值是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
原因分析:
-
let 的作用域 :
let是块级作用域,每次循环都会创建一个新的a -
闭包捕获 :每个
setTimeout回调函数都捕获了各自循环迭代时的a -
独立变量 :实际上有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 |
|---|---|---|
| 作用域 | 函数/全局作用域 | 块级作用域 |
| 变量提升 | 是 | 否(暂时性死区) |
| 循环中 | 共享同一个变量 | 每次迭代创建新变量 |
| 闭包问题 | 需要额外处理 | 自动捕获迭代值 |
| 推荐程度 | ❌ 现代开发避免 | ✅ 推荐使用 |
关键点:
-
var在循环中会创建闭包问题 -
let为每次循环创建新的词法环境 -
理解这个区别对处理异步编程至关重要
-
现代JavaScript开发应该优先使用
let/const