
本文详解 JavaScript 循环中使用 var 与 let 声明迭代变量时,闭包捕获行为的根本差异,揭示块级作用域如何为每次循环迭代创建独立绑定,从而解决“所有函数返回同一值”的经典陷阱。
本文详解 javascript 循环中使用 `var` 与 `let` 声明迭代变量时,闭包捕获行为的根本差异,揭示块级作用域如何为每次循环迭代创建独立绑定,从而解决“所有函数返回同一值”的经典陷阱。
在 JavaScript 开发中,一个高频且易错的场景是:在循环中创建多个闭包,期望每个闭包“记住”当前迭代的变量值,却意外发现所有闭包都返回最终的迭代值(如 10)。这一现象背后,本质是变量作用域、声明提升与闭包内存模型三者交织的结果。下面我们将从原理到实践,系统梳理其运行机制。
? 根本原因:var 的函数作用域 vs let 的块级迭代绑定
关键不在于“是否复制变量”,而在于每次循环是否生成新的词法环境(Lexical Environment)和对应的变量绑定(binding)。
使用 var i 时,i 被提升至整个 constfuncs 函数作用域顶部,仅存在一个 i 绑定。所有箭头函数共享该单一绑定;循环结束后 i === 10,因此 funcs[5]() 返回 10。
-
使用 let i 时,ECMAScript 规范明确要求:for 循环中用 let/const 声明的初始化变量,会在每次迭代开始前,为该次迭代创建一个全新的词法环境,并在其中重新绑定 i(参见 ES2023 §14.7.2)。这意味着:
立即学习“Java免费学习笔记(深入)”;
- 迭代 0 → 创建绑定 i@0,值为 0
- 迭代 1 → 创建绑定 i@1,值为 1
- …
- 迭代 5 → 创建绑定 i@5,值为 5
- 每个闭包捕获的是各自迭代独有的 i 绑定,而非共享同一个变量。
✅ 正确示例(let 在 for 初始化中):
function constfuncs() { const funcs = []; for (let i = 0; i < 10; i++) { funcs[i] = () => i; // 每个函数捕获自己迭代的 `i` 绑定 } return funcs; }
const funcs = constfuncs(); console.log(funcs[0]()); // 0 console.log(funcs[5]()); // 5 console.log(funcs[9]()); // 9
> ❌ 错误变体(`let` 声明在循环外):
```javascript
function constfuncs() {
const funcs = [];
let i; // 单一绑定,作用域为整个函数
for (i = 0; i < 10; i++) {
funcs[i] = () => i; // 全部捕获同一个 `i`
}
return funcs;
}
// 所有 funcs[n]() 都返回 10✅ 替代方案(var + 内部 let):
function constfuncs() { const funcs = []; for (var i = 0; i < 10; i++) { let value = i; // 每次迭代新建块级绑定 funcs[i] = () => value; } return funcs; } // 同样正确:`value` 是每次迭代独立的绑定
? 关于闭包与内存:不是“拷贝值”,而是“捕获绑定”
需要澄清一个常见误解:闭包并未将变量值从栈复制到堆,也不涉及传统意义上的“内存拷贝”。JavaScript 引擎(如 V8)采用基于词法环境链(Lexical Environment Chain) 的实现:
- 每个函数对象内部持有一个对其定义时所在词法环境的引用;
- 当 let i 在 for 中声明时,引擎为每次迭代动态创建一个新词法环境(包含该次 i 的绑定),并让对应闭包指向它;
- 变量值本身存储在引擎管理的堆内存中,绑定(binding)则是词法环境中对值的“指针式关联”。
因此,“每个闭包访问自己的 i”的本质是:它们指向不同的词法环境对象,而这些对象各自持有独立的 i 绑定 —— 这正是规范所定义的“每次迭代独立绑定”(independent binding per iteration)。
⚠️ 注意事项与最佳实践
- 不要依赖 var 实现循环闭包隔离:这是历史遗留缺陷,应完全避免;
- let 的迭代绑定是 for 语句的特例:仅适用于 for (let x;;) 形式;for...of 和 for...in 同样适用,但 while 或手动递归中需显式声明块级变量;
- const 同理:for (const i = 0; i < 10; i++) 会报错(i 不可重赋值),但 for (const item of arr) 安全,因每次迭代 item 是新绑定;
- Babel 等转译器的模拟:在 ES5 环境中,常通过 IIFE(立即执行函数)+ 参数传入模拟 let 行为,本质也是创建新作用域绑定。
✅ 总结
let i 在 for 循环中的魔力,源于语言规范对“迭代绑定”的强制语义:每一次循环都开辟一个独立的块级作用域,并在其中建立专属的变量绑定。闭包捕获的不是变量名,而是该名称在特定词法环境中的绑定关系。理解这一点,便能穿透语法表象,掌握 JavaScript 作用域与闭包协同工作的底层逻辑 —— 这不仅是修复 bug 的钥匙,更是构建可预测、可维护异步与回调逻辑的基石。








