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

相关推荐
pixle018 分钟前
Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码
前端·3d·echarts
麻芝汤圆1 小时前
MapReduce 入门实战:WordCount 程序
大数据·前端·javascript·ajax·spark·mapreduce
juruiyuan1113 小时前
FFmpeg3.4 libavcodec协议框架增加新的decode协议
前端
Peter 谭3 小时前
React Hooks 实现原理深度解析:从基础到源码级理解
前端·javascript·react.js·前端框架·ecmascript
周胡杰4 小时前
鸿蒙接入flutter环境变量配置windows-命令行或者手动配置-到项目的创建-运行demo项目
javascript·windows·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
LuckyLay4 小时前
React百日学习计划——Deepseek版
前端·学习·react.js
gxn_mmf5 小时前
典籍知识问答重新生成和消息修改Bug修改
前端·bug
hj10435 小时前
【fastadmin开发实战】在前端页面中使用bootstraptable以及表格中实现文件上传
前端
乌夷5 小时前
axios结合AbortController取消文件上传
开发语言·前端·javascript
晓晓莺歌5 小时前
图片的require问题
前端