let在for循环中每次迭代创建独立绑定,使异步回调捕获当轮i值;var共用同一变量,导致所有回调读取最终值。

关键在于:let 声明在 for 循环中不是“复用一个变量”,而是每次迭代都创建一个全新的绑定(binding)。这个新绑定属于当前迭代的块级作用域,和上一轮、下一轮的 i 互不干扰。
为什么 let i 在循环里不会被覆盖?
传统认知里,“i++”像是在改同一个变量;但 let 的语义不同:
- for (let i = 0; i < 3; i++) { ... } 中,引擎为第 0 轮、第 1 轮、第 2 轮分别生成三个独立的 i 绑定(比如叫 i₀、i₁、i₂)
- 每轮循环体执行时,访问的是“本轮专属”的那个 i,而不是共享的全局或函数级变量
- 即使异步回调(如 setTimeout)延后执行,它捕获的仍是那个特定轮次的绑定值
对比 var 就很清楚了
var 声明的 i 是函数作用域或全局作用域的单一变量,所有迭代共用同一个内存位置:
- 循环结束时 i 已变成 3(或超出上限的值)
- 所有 setTimeout 回调读到的都是这个最终值,所以输出全是 3
- 而 let 让每个回调“记住自己那轮的 i”,输出 0、1、2
注意:不是“作用域块”本身起作用,而是“每次迭代新建绑定”
很多人误以为只要用了 let 就自动有了块级作用域,就能解决循环问题——其实核心是迭代绑定机制:
立即学习“Java免费学习笔记(深入)”;
- 哪怕写成 for (let i = 0; i < 3; i++) { if (true) { console.log(i); } },if 块只是普通块,真正隔离靠的是 for 迭代本身的绑定行为
- ES6 规范明确要求:for/for-of/for-in 中的 let/const 声明,在每次迭代开始时都会初始化一个新绑定
- 这个机制与闭包无关,但效果类似——每个回调自然持有自己那轮的值
一个容易误解的反例
下面代码输出 0、0、0,不是因为 let 失效,而是 getI 和 incrementI 在循环开始前就定义并捕获了初始 i 绑定:
for (let i = 0, getI = () => i, incrementI = () => i++; getI() < 3; incrementI()) {
console.log(i);
}
这里 getI 和 incrementI 是在“初始作用域”中创建的,它们始终操作最开始的那个 i,而不是每轮新建的 i。这说明:绑定时机很关键——函数定义的位置决定了它捕获哪个绑定。


















