前言
这篇文章的起因要从一道题说起:
for(let i=0;i<3;i++){
i = 'haha';
console.log(i);
}
for(let i=0;i<3;i++){
let i = 'haha';
console.log(i);
}
一、起因
我预料的答案是和
,
但浏览器给的答案是和
很好,你成功引起了我的注意~。
二、定位
1.加一加断点
我们先加入断点,看看究竟是怎么回事。代码如下(示例):
for(let i=0;i<3;i++){
i = 'haha';
debugger;
console.log(i);
}
for(let i=0;i<3;i++){
let i = 'haha';
debugger;
console.log(i);
}
结果如图:
根据scope链我们可以看出,第一题中,我们改变了变量i为“haha”,i++后为NaN,无法通过测试语句,因此只会循环一次。第二题中,两个“i”变量分别位于不同的Block中,因此没有相互影响。over,我们下篇文章再见~
当然不能这么简单,我们先看看有没有先例文章能参考一下~~
2.翻一翻互联网
找到了一篇文章(juejin.cn/post/688002…
又找到了一篇文章(juejin.cn/post/689969…
根据这两篇文章的关键内容,我们可以知道,for(let 的循环体是有一个新的作用域inner,而变量i来自他的父作用域for
,因此两个变量i之间不会有影响。
看起来我们找到了答案,但是,我要说但是了,第一题的scope链条为什么会比第二题少一个, 这个逻辑和第二题的表现也不符合。看来我们得去看一下相关规范了~~(out,for,inner这三个作用域记一下,我们后面会用到)
3.瞅一瞅规范
在我们正式开始看规范之前,先做一下必备知识的铺垫工作:
名词解释:
-
the running execution context:运行时执行上下文,当程序运行,进入到某段代码块时,一个新的执行上下文被创建,并被放入一个 stack 中。当程序运行到这段代码块结尾后,对应的执行上下文被弹出 stack,就是运行时执行上下文。一般包括Lexical Environments(词法环境)和VariableEnvironment (可变环境)
-
LexicalEnvironment:词法环境,记录 let、const 的声明。一般包括Environment Record(环境记录)和Reference to an outer Lexical Environment(外部 lexical environment 的引用)
-
VariableEnvironment:可变环境,记录var 声明的变量,也是LexicalEnvironment的实例
-
Environment Record:环境记录,存储变量,函数等声明的实际映射,一般包括:Declarative environment record(声明性环境记录) , Object environment record(对象环境记录),Global Environment Records(全局环境记录)
-
Declarative environment record:声明性环境记录,存储变量,函数(可绑定this),常量,类,模块声明
-
Object environment record:对象环境记录,存储对象声明
-
Global Environment Records:全局环境记录,存储全局变量,全局对象的属性以及脚本出现的所有顶级声明,可绑定this
太多不看:
执行上下文包括词法环境和可变环境,二者合称作用域。词法环境拥有外部词法环境的引用,这就是作用域链。
有了铺垫知识,我们可以正式开始瞅一瞅规范了,链接在此262.ecma-international.org/6.0/#sec-fo…,关键流程图是这个
其实重点方法只有3个,我们一个一个看,先看LabelledEvaluation:
关键点:
oldEnv:for循环外部的词法环境,相当于前面文章提到的outer作用域
loopEnv:for语句的词法环境,想当于前面文章提到的for作用域
将当前执行上下文的词法环境指向loopEnv,以perIterationLets(包含变量名i)为参数执行 ForBodyEvaluation方法
我们再看ForBodyEvaluation方法:
可以看到,ForBodyEvaluation方法执行后不久,就以perIterationLets(包含变量名i)为参数执行了CreatePerIterationEnvironment方法,我们不管后面的代码,先看看CreatePerIterationEnvironment做了什么。
关键点:
我们先假设是i=0的那次循环,那么,
lastIterationEnv :此时指的是loopEnv,也就是前面文章提到的for作用域
outer :此时指的是oldEnv,也就是前面文章提到的outer作用域
thisIterationEnv :循环体的词法环境,我们可以暂时叫它insideEnv_0,就是文章前面提到的inner作用域。insideEnv_0的父词法环境是oldEnv
换句话说for作用域和循环体作用域inner是兄弟关系。
**红框部分是将for作用域下perIterationLets的变量都复制到inner作用域,**最后执行上下文的词法环境指向insideEnv_0。
现在,我们再回到ForBodyEvaluation方法中:
关键点:
执行完循环体语句后,会再走CreatePerIterationEnvironment方法,注意现在执行上下文的词法环境是insideEnv_0,**因此将创建一个新的循环体词法环境insideEnv_1,insideEnv_1和insideEnv_0是兄弟环境,它们有共同的父环境oldEnv,insideEnv_1的i变量的值来自insideEnv_0,**然后将执行上下文的词法环境指向insideEnv_1。
接下来走自增语句逻辑(i++),此时的i其实是insideEnv_1中的变量i,然后开启下一个循环。
到这里我们会发现,loopEnv(for作用域)只是完成了i变量的第一次声明,后续的test语句(i<3),自增语句(i++)用到的都是insideEnv(inner作用域)中的变量i。
规范已经看完了,接下来我们再来看看那两道题目~~
4.给一给答案
第一题,根据我们了解的规范知识:
循环体语句结束后,会根据insideEnv_0创建一个兄弟环境insideEnv_1,里面的i变量值也是“haha”,自增语句(i++)后i为NaN,无法通过test语句(i<3),因此该循环只会执行一次。
第二题稍有不一样:
循环体语句使用了let声明,会以insideEnv_0为父环境创建一个新的词法环境letEnv(规范参考:262.ecma-international.org/6.0/#sec-bl… ,并将执行上下文的词法环境指向letEnv,然后声明一个“新变量i”并赋值“haha”。循环体语句结束后,执行上下文的词法环境被还原成insideEnv_0,此时insideEnv_0中的变量i为0,自增语句(i++)后i为1,顺利通过test语句(i<3),进入下一次循环。
三、总结(太长不看系列)
一图胜千言:
// oldEnv for循环开始之前的词法环境
for(let i=0;i<3;i++){ // loopEnv for循环语句的词法环境,父环境是oldEnv,声明有变量i
// insideEnv,循环体的词法环境,和loopEnv是兄弟关系,父环境也是oldEnv
// 变量i从loopEnv复制而来,是真正用来做自增语句和判断语句的执行
}
// for循环中使用let的scope链条是oldEnv——insideEnv——其他(blcok)
// 其中insideEnv会有多个,用来解决for(var i=0)的老生常谈的问题
好的,文章到这里就结束了,有疑问或觉得不对的地方欢迎在评论中提出,我们一起探讨,也请大家一键三连,我们下篇文章再见~~
四、参考文章
- JS作用域与闭包 let在for循环中的使用:juejin.cn/post/688002…
- ES6中let在for循环中的表现:juejin.cn/post/689969…
- 学习公式——执行上下文构成:juejin.cn/post/706713…