作用域链——for循环中的let

550 阅读3分钟

前言

​ 这篇文章的起因要从一道题说起:

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);
}

一、起因

​ 我预料的答案是1645501017072.png1645447743082.png

但浏览器给的答案是1645501076340.png1645448021809.png

​ 很好,你成功引起了我的注意~。

二、定位

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);
}

​ 结果如图:

1645501270803.png

1645448258475.png

​ 根据scope链我们可以看出,第一题中,我们改变了变量i为“haha”,i++后为NaN,无法通过测试语句,因此只会循环一次。第二题中,两个“i”变量分别位于不同的Block中,因此没有相互影响。over,我们下篇文章再见~

​ 当然不能这么简单,我们先看看有没有先例文章能参考一下~~

2.翻一翻互联网

​ 找到了一篇文章(juejin.cn/post/688002…

1645497529262.png

​ 又找到了一篇文章(juejin.cn/post/689969…

1645498078022.png

​ 根据这两篇文章的关键内容,我们可以知道,for(let 的循环体是有一个新的作用域inner1645520494551.png,而变量i来自他的父作用域for1645520636018.png,因此两个变量i之间不会有影响。

​ 看起来我们找到了答案,但是,我要说但是了,第一题的scope链条为什么会比第二题少一个,1645521058935.png 这个逻辑和第二题的表现也不符合。看来我们得去看一下相关规范了~~(out,for,inner这三个作用域记一下,我们后面会用到

3.瞅一瞅规范

​ 在我们正式开始看规范之前,先做一下必备知识的铺垫工作:

image.png名词解释:

  1. the running execution context:运行时执行上下文,当程序运行,进入到某段代码块时,一个新的执行上下文被创建,并被放入一个 stack 中。当程序运行到这段代码块结尾后,对应的执行上下文被弹出 stack,就是运行时执行上下文。一般包括Lexical Environments(词法环境)和VariableEnvironment (可变环境)

  2. LexicalEnvironment:词法环境,记录 let、const 的声明。一般包括Environment Record(环境记录)和Reference to an outer Lexical Environment(外部 lexical environment 的引用)

  3. VariableEnvironment:可变环境,记录var 声明的变量,也是LexicalEnvironment的实例

  4. Environment Record:环境记录,存储变量,函数等声明的实际映射,一般包括:Declarative environment record(声明性环境记录) , Object environment record(对象环境记录),Global Environment Records(全局环境记录)

  5. Declarative environment record:声明性环境记录,存储变量,函数(可绑定this),常量,类,模块声明

  6. Object environment record:对象环境记录,存储对象声明

  7. Global Environment Records:全局环境记录,存储全局变量,全局对象的属性以及脚本出现的所有顶级声明,可绑定this

太多不看:

执行上下文包括词法环境和可变环境,二者合称作用域。词法环境拥有外部词法环境的引用,这就是作用域链。

​ 有了铺垫知识,我们可以正式开始瞅一瞅规范了,链接在此262.ecma-international.org/6.0/#sec-fo…,关键流程图是这个1645522477783.png

其实重点方法只有3个,我们一个一个看,先看LabelledEvaluation:

1645528594093.png

关键点:

oldEnv:for循环外部的词法环境,相当于前面文章提到的outer作用域

loopEnv:for语句的词法环境,想当于前面文章提到的for作用域

将当前执行上下文的词法环境指向loopEnv,以perIterationLets(包含变量名i)为参数执行 ForBodyEvaluation方法

我们再看ForBodyEvaluation方法:

1645529571194.png

​ 可以看到,ForBodyEvaluation方法执行后不久,就以perIterationLets(包含变量名i)为参数执行了CreatePerIterationEnvironment方法,我们不管后面的代码,先看看CreatePerIterationEnvironment做了什么。

1645530182574.png 关键点:

​ 我们先假设是i=0的那次循环,那么,

lastIterationEnv :此时指的是loopEnv,也就是前面文章提到的for作用域

outer :此时指的是oldEnv,也就是前面文章提到的outer作用域

thisIterationEnv :循环体的词法环境,我们可以暂时叫它insideEnv_0,就是文章前面提到的inner作用域。insideEnv_0的父词法环境是oldEnv

换句话说for作用域和循环体作用域inner是兄弟关系。

​ **红框部分是将for作用域下perIterationLets的变量都复制到inner作用域,**最后执行上下文的词法环境指向insideEnv_0。

现在,我们再回到ForBodyEvaluation方法中:

1645533123581.png 关键点:

​ 执行完循环体语句后,会再走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.给一给答案

第一题,根据我们了解的规范知识:

1645584871181.png 循环体语句结束后,会根据insideEnv_0创建一个兄弟环境insideEnv_1,里面的i变量值也是“haha”,自增语句(i++)后i为NaN,无法通过test语句(i<3),因此该循环只会执行一次。

第二题稍有不一样:

1645585284142.png

​ 循环体语句使用了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)的老生常谈的问题

1645601465490.png好的,文章到这里就结束了,有疑问或觉得不对的地方欢迎在评论中提出,我们一起探讨,也请大家一键三连,我们下篇文章再见~~

四、参考文章