JavaScript for循环中let的变量绑定与作用域机制疑问解答
为什么let声明的循环迭代变量每轮有独立绑定
首先对比var的行为:var是函数作用域声明,用var声明循环变量i时,整个循环共享同一个i绑定。等100ms后定时器回调执行时,循环早就跑完,i已经累加到3,所以三个回调都会打印3。
let在for循环头部声明迭代变量时,会触发ECMAScript规范明确规定的特殊逻辑:每一轮迭代启动时,引擎都会为当前轮次创建一个全新的i绑定,把上一轮迭代结束时的i值拷贝到新绑定中。循环体内的所有代码(包括延迟执行的闭包)捕获的都是当前轮次自己的i绑定,不会互相干扰,所以示例代码最终会依次打印0、1、2:
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i) }, 100); }
这个行为不是循环“自动识别”闭包需求,是规范层面的强制约定,从语言设计上解决了早年var带来的循环闭包经典坑。
要注意这个特殊逻辑的触发条件非常严格:只有直接在for/for-in/for-of循环的头部声明位置用let/const定义迭代变量时才会生效。如果是在循环外部先声明let i,再在循环头里给i赋值,就不会产生每轮独立绑定,效果和var类似,最终会打印三个3:
let i; for (i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 三个回调都引用外层的同一个i,最终输出3个3 }
不存在独立的“循环作用域”,本质仍是块级作用域规则
JS确实没有第四种“循环作用域”,所有作用域都可以归为全局作用域、函数作用域、块级作用域三类,大家平时听到的“循环作用域”只是对for循环特殊绑定逻辑的通俗简化说法。
你觉得迭代变量i声明在循环体{}外部、词法作用域覆盖整个循环,是因为for循环的头部和循环体实际对应两层嵌套的块级作用域:
- 外层块作用域对应整个for循环结构,循环头声明的初始
i绑定就存在这一层,负责循环条件判断、迭代更新(也就是i<3、i++这部分逻辑)的执行。 - 每一轮循环执行时,引擎都会为循环体
{}创建一个全新的内层块作用域,把当前轮次的i值绑定到这个内层作用域中。循环体内访问i时,实际访问的是当前轮次内层块的独立绑定,不是外层那个用来控制循环的共享i。
这个两层作用域的逻辑可以用一段代码直接验证:
for (let i = 0; i < 3; i++) { let i = 999; // 内层块可以重复声明i,和外层循环控制的i互不干扰 setTimeout(() => console.log(i), 100); } // 最终输出3个999,足以证明循环体和循环头的i不在同一个作用域
说白了,for循环的迭代变量特殊行为,本质是块级作用域规则+规范给循环头let声明加的“每轮拷贝绑定”的特殊逻辑,根本没有新增作用域类型。
内容的提问来源于stack exchange,提问作者user10869882

