前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
for 语句
对应协议的 14.7.4 The for Statement。
先看两段代码
javascript
for(var varA = 0; varA < 3 ; varA++)
console.log(varA);
console.log(varA)
// 0
// 1
// 2
javascript
for(let letA = 0; letA < 3 ; letA++)
console.log(letA);
console.log(letA);
// 0
// 1
// Uncaught ReferenceError: constA is not defined
提一个问题, 为啥 letA
脱离 for 循环之后,访问不到了,而 varA
可以。
块(代码块)的let/const/class的申明只在块内有效,这个大家都知道, 可是这里也没有块啊。
回顾回顾之前的 作用域链章节 块的的机制,本质是啥?
- 进入 块时,新建了 申明环境记录,用于保存 词法申明,同时修改执行上下文和相关环境记录的指向
- 离开 块时,恢复执行上下文的环境记录执行
再简单一点,就是动态改变了 作用域链(环境记录链路)。
所以for let 的本质是差不多的,但又有一些特别。
for var
先看看协议对 for var 的 评估

这比较抽象,一起看关系图
for var 进入前后对比

执行到 console.log(varA)
的时候

当 for var
执行完毕后,上述的关系图没有任何变化(值是有变化的), 所以依旧可以访问到 varA
for let
协议对 for let/const/class这种情况 的 评估, 会多出一个 loopEnv
环境记录,这就是和 fort var的不同。
一起对着代码和协议一起追查:

- 每次for/let有新建申明环境记录
- for循环里面的语句执行前,更改了执行上下的LexicalEnvironment 指向了 新的申明环境记录
- or循环里面的语句执行后,恢复了LexicalEnvironment指向。
依旧一起看进入 for let 前后的关系图

执行到 console.log(letA)
时, 关系图如下:
所以在执行上下文,在整个大 for 循环语句里面能访问到 letA, 同时通过 [[OuterEnv]]
形成的链路,能访问到全局环境记录中的 标志符。

离开 for 循环之后,执行上下文LexicalEnvironment
恢复到执行前,重新指向全局环境记录,关系图,更改如下:
这个时候的执行上下文 关联的 环境记录上,已经无法找到 letA
了。

已解答:
为啥 letA
脱离 for 循环之后,访问不到了,而 varA
可以。
闭包经典案例
从上面的分析已知,for let
通过新建申明记录来保存 词法申明,进入前后通过更改下 执行上下文的 LexicalEnvironment
来修改环境记录的关联链路, 从而实现 letA
脱离 for 循环之后,就无法访问。
对于如下的 for var
代码:
从上面得知,varA
是在全局环境记录上对象环境记录的建立的绑定关系,所以延时输出时, 都是直接从全局环境记录中取值的,稳定的输出 3
是符合预期的。
javascript
for (var varA = 0; varA < 3; varA++) {
setTimeout(() => {
"use strict"
console.log(`varA:${varA}`)
}, varA * 1000)
}
// varA:3
// varA:3
// varA:3
但是如下的现象,又作何解释,
javascript
for (let letA = 0; letA < 3; letA++) {
setTimeout(() => {
"use strict"
console.log(`letA:${letA}`)
}, letA * 1000)
}
// letA:0
// letA:1
// letA:2
先画出执行到 console.log(letA:${letA}
) 的关系图 (可能有疏漏,大致的链路问题不大)
- Block 块会新建一个申明环境记录
- setTimeout 的回调函数会新建一个 函数环境记录
这个图能解释,为什么 setTimeout的回调函数能访问到 letA
。

但是,但是,但是, 重要的问题说三遍,不能解释 每次 letA
的输出不一样。 因为 这里的for 循环本身是同步的,如果整个循环 for let
遍历时申明环境是同一个,那么setTimeout 的回调函数输出的值也是一致的。
如果再加上一个条件, 每次的 申明环境记录,都是独立的呢? 就可以解释得通,协议也是这么处理的。
简化的逻辑如下:
- 进入
for let
时,会新建一个申明环境记录 loopEnv, 并将 执行上下文LexicalEnvironment
指向looEnv for let
的第一条词法申明语句let letA = 0
执行完毕后,词法申明的标志符绑定了值- 每次
for let
循环的时候 (核心之核心)- lastIterationEnv = 执行上下文.
LexicalEnvironment
- 会新建一个新申明环境记录 thisIterationEnv, 其
[[outerEnv]]
保持和 lastIterationEnv 的一致,即外围的链路保持一致。 - thisIterationEnv 拷贝 所有 lastIterationEnv的标志符绑定关系
- 执行上下文.
LexicalEnvironment
指向 thisIterationEnv
- lastIterationEnv = 执行上下文.
对应这协议的 14.7.4.3 ForBodyEvaluation

到这里基本都OK了,还有一个之前提到的问题(闭包问题),setTimeout的回调函数执行时,是怎么找到其外围的环境记录的,答案就是 函数对象 (function object) 的 [[Environment]]
指向着相关环境记录。详细的细节看闭包章节。
所以 本示例三次执行的 关系图如下:



for-in、 for-of 和 for-waiting-of 语句
先看两个简单的例子
- for-in 和 for-of 语句,若 in/of 的左侧有词法申明,依旧是离开了语句就失效了(不考虑闭包的特殊情况)。
javascript
var obj = { p1: "p1", p2: "p2" };
for (let p in obj) console.log(p) // p1, p2
console.log(p); // undefined
for (var p in obj) console.log(p) // p1, p2
console.log(p); // p2
javascript
var arr = [1, 2];
for (var v of arr) console.log(v); // 1, 2
console.log(v); // 2
arr.push(3)
for (let v of arr) console.log(v); // 1, 2, 3
console.log(v); // 2
for-in 是遍历键,而 for-of 是遍历值,其有多种组成结构。

for-in
, for-of
和 for-waiting-of
相关描述在协议的 14.7.5 The for-in, for-of, and for-await-of Statements。各种模式都分为 ForIn/OfHeadEvaluation 和 ForIn/OfBodyEvaluation ,即头部和主体(身体)两部分。

ForIn/OfHeadEvaluation
ForIn/OfHeadEvaluation 最终返回的是 Iterator Record或者非正常结束。
for-in
为例, 其逻辑就是
- 利用 In 左边的 LeftHandSideExpression , ForBinding 或者 ForDeclaration
- 和In右边的 表达式(Expression )
生成 Iterator Record,然后作为参数传递给 ForIn/OfBodyEvaluation

Iterator Record是什么呢? 看这个表格, 就很清楚了。

利用 [[NextMethod]]
取值, [[Done]]
标记是否已经枚举完毕。
反复操作,即完成遍历操作。
ForIn/OfBodyEvaluation
ForIn/OfBodyEvaluation, 但是对应场景太多,这里就简单解释最简单的 for-in 和 for-of
有词法申明的场景, 即红色圈出的部分的逻辑:

核心逻辑就是 对 ForIn/OfHeadEvaluation 生成 iteratorRecord ( Iterator Record) 反复取值,即遍历
- 如果有词法申明的情况,会新建一个新的申明环境记录,用于保存词法申明绑定关系
- 实例化 申明环境记录,更改执行上下文
LexicalEnvironment
的指向,以及初始化词法绑定 - 评估 ForIn 或者 forOf Body(主体)部分的代码
- 恢复指向上下文的
LexicalEnvironment
的指向
重点看下面红色标注的逻辑即可

基本思路是和 之前的 for 是类似的, 都是每次遍历动态创建一个新的环境记录,执行上下文在动态的修改 LexicalEnvironment 的指向,从而实现了 动态的环境记录链路。
思考
虽然 for 语句 与 for-in , for-of , for-waiting-of 在对词法申明实现的思路基本是一致的,但是对遍历本身的实现却是不一样的,
for 语句是显示的判断退出条件, for-in , for-of , for-waiting-of
语句,显然也是可以退出的,那退出逻辑和取值逻辑又是怎么样的,这和前面提到 Iterator Record密切相关。