ECMAScript for, for-in for-of 基本原理

前言

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

对应这协议的 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-infor-offor-waiting-of 相关描述在协议的 14.7.5 The for-in, for-of, and for-await-of Statements。各种模式都分为 ForIn/OfHeadEvaluationForIn/OfBodyEvaluation ,即头部和主体(身体)两部分。

ForIn/OfHeadEvaluation

ForIn/OfHeadEvaluation 最终返回的是 Iterator Record或者非正常结束。

for-in为例, 其逻辑就是

生成 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密切相关。

相关推荐
高木的小天才5 分钟前
鸿蒙中的并发线程间通信、线程间通信对象
前端·华为·typescript·harmonyos
Danta1 小时前
百度网盘一面值得look:我有点难受🤧🤧
前端·javascript·面试
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
dwqqw1 小时前
opencv图像库编程
前端·webpack·node.js
Captaincc2 小时前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚2 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿3 小时前
MCP
前端
layman05283 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝3 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML3 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能