for 循环里 var 和 let 的经典区别,本质不是“一个旧一个新”,而是它们对每次迭代变量的作用域处理不同。你看到 3 3 3 或 0 1 2,其实是在观察闭包到底记住了“同一个变量”,还是“每轮一个新变量”。
目录
先看经典例子
function createCounters() {
const counters = []
for (var i = 0; i < 3; i++) {
counters.push(function () {
return i
})
}
return counters
}
调用:
const counters = createCounters()
console.log(counters[0]())
console.log(counters[1]())
console.log(counters[2]())
结果是:
3
3
3
为什么是 3 3 3
因为闭包记住的不是“当时的数值副本”,而是变量 i 本身。var 是函数作用域,在整个 for 循环里只有一个共享的 i。
循环跑完后:
i === 3
所以三个函数最后读到的都是同一个最终结果。
换成 let 为什么就变了
function createCounters() {
const counters = []
for (let i = 0; i < 3; i++) {
counters.push(function () {
return i
})
}
return counters
}
现在输出会变成:
0
1
2
原因是 let 是块级作用域,循环的每一轮都会得到一个新的 i 绑定。闭包拿到的是每一轮自己的那个 i,而不是全局共享的一份。
真正要记住的不是“let 更高级”
而是:
var:整个函数里共享一个变量let:每轮迭代都可能生成新的绑定
所以问题不在 for 本身,而在“闭包捕获的到底是哪一个变量绑定”。
IIFE 以前为什么能解决
在 let 普及之前,常见写法是 IIFE:
function createCounters() {
const counters = []
for (var i = 0; i < 3; i++) {
;(function (j) {
counters.push(function () {
return j
})
})(i)
}
return counters
}
这里的关键不是 IIFE 本身神奇,而是它人为创建了一个新的函数作用域,并把当前 i 作为参数值传给了 j。于是每次闭包都抓到了不同的 j。
在现代代码里,如果没有兼容性包袱,直接用 let 更清楚。
什么时候这个坑最容易出现
for循环里创建回调setTimeout- 事件绑定
- 异步任务队列
例如:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
依然会打印三次 3。
和闭包的关系到底是什么
闭包不是 bug,闭包只是“函数记住外部变量”的能力。真正让结果不同的是:
- 你记住的是一个共享变量
- 还是每轮独立的变量绑定
这也是为什么闭包、作用域和循环变量这三个概念总会在一起出现。
结论
var在for里通常会让多个闭包共享同一个变量。let会为每轮迭代创建新的绑定,所以更符合直觉。- IIFE 是旧时代为了解决
var共享变量问题的常见手段。
如果你接下来还在处理循环、遍历和数组回调的边界,可以接着看 JavaScript 数组与遍历指南;如果你在处理对象空值链路,继续看 可选链指南。
继续阅读
JavaScript 中 var 和 let 的作用域差异解析
作用域定义了变量和函数的可访问范围。函数作用域:变量在整个函数内都有效,通常由var创建。块级作用域:变量只在其所在的代码块内有效,通常由let和const创建。在 JavaScript 中,var和letvar声明的变量具有函数作用域,这意味着它在整个函数内都有效。由于var的
8 分钟TypeScript 里 as 和 satisfies 怎么选
解释 TypeScript 中 `as`、尖括号断言和 `satisfies` 的差异,重点说明为什么 `satisfies` 更适合对象结构校验,而 `as` 更像是在告诉编译器“先按我说的算”。
3 分钟console.dir 和 console.log 有什么区别
说明 console.log 与 console.dir 在浏览器 DevTools 里的实际差异,重点回答对象调试、DOM 节点查看和 Node.js 环境下什么时候该用哪一个。
订阅 FreeMac
每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。