你提出的这两段代码非常经典,很好地展示了 JavaScript 中 var 和 let 在作用域和闭包行为上的关键区别。
🔍 问题重现:
// 使用 var
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
// 使用 let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
我们来分别分析它们的输出结果和背后原理。
✅ 第一段:使用 var
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
❓ 输出什么?
5
5
5
5
5
🧠 原因解释:
var是函数作用域(function-scoped),在全局或函数中声明的变量会提升并共享同一个作用域。- 这个循环中的
i是一个全局变量(或者函数作用域变量)。 - 所有的
setTimeout回调函数都引用的是 同一个变量i。 - 当
setTimeout真正执行时(事件循环之后),循环已经结束,此时i的值是5。 - 所以所有的回调都打印出
5。
✅ 第二段:使用 let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
}
❓ 输出什么?
0
1
2
3
4
🧠 原因解释:
let是块级作用域(block-scoped),在{}内部定义的变量只在该块内有效。- 更重要的是,在
for循环中使用let声明变量时,JavaScript 会在每次迭代中创建一个新的绑定(即新的作用域)。 - 每次循环体中创建的
setTimeout回调函数都会捕获当前迭代中的i值。 - 所以每个回调函数打印出的是各自迭代中被“记住”的
i值。
💡 形象比喻
你可以把这两个例子想象成:
使用 var | 使用 let |
|---|---|
所有回调函数共用一盏灯(同一个 i 变量) | 每次循环都有一盏新灯(独立的 i) |
| 最后灯关了(i=5),所有人都看到黑 | 每个人记住自己当时看到的灯光亮度 |
🛠️ 如果你想用 var 达到 let 的效果怎么办?
可以通过 IIFE(立即执行函数表达式) 来手动创建作用域:
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
});
})(i);
}
这样也能输出:
0
1
2
3
4
因为 IIFE 给每次循环创建了一个新的作用域,并把当时的 i 值传进去保存下来。
✅ 总结对比表:
| 特性 | var i | let i |
|---|---|---|
| 作用域 | 函数/全局作用域 | 块级作用域 |
| 每次循环是否创建新变量 | 否 | 是 |
setTimeout 捕获的值 | 最终的 i(5) | 每次循环的当前 i |
| 输出结果 | 5 5 5 5 5 | 0 1 2 3 4 |
| 推荐使用 | ❌ 不推荐用于异步回调 | ✅ 推荐 |
如果你想了解更多关于作用域、闭包、事件循环等概念,也欢迎继续提问!😊