【人人都能读标准】10. 作用域链与闭包

news/2024/4/28 4:11:30/文章来源:https://blog.csdn.net/weixin_44384265/article/details/129676443

本文为《人人都能读标准》—— ECMAScript篇的第10篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。


在8.执行环境我们说过,由ECMAScript代码创建的执行上下文会有一个词法环境的组件,指向一个环境记录器,这是每一条作用域链的起点。

在9.作用域我们提到,每一个环境记录器都有一个[[OuterEnv]]字段指向另一个环境记录器,通过[[OuterEnv]]连接起来的所有环境记录器共同构成一条作用域链。并且,在那一节中我们已经多多少少已经见过作用域链在环境中的样子了:

for (var i = 1; i < 5; i++) {setTimeout(() => {console.log(i) //  ①}, i * 1000)
}

第一次执行到位置 ①,调用栈如下所示:

open-pic

本节,我会先讲标识符解析的算法,即在作用域链上查找标识符的具体过程;然后,我会讲作用域链的构建过程;最后,我会讲一个以作用域链为基础、且大家都非常关心的概念 —— 闭包。


标识符的解析

在执行代码的过程中,当需要解析某个标识符的时候,会获取当前执行上下文的词法环境,从词法环境指向的环境记录器开始,沿着作用域链依次查找标识符,直到找到标识符或者走完整条作用域链才结束。

我们可以从标识符identifier的求值语义看到详细的过程:

resolvebinding

以下我为你把这个过程使用中文进行概括(name为标识符):

  1. 执行ResolveBinding(name):
    1. 默认把变量env设置为当前执行上下文的词法环境;(
    2. 此时env是一个环境记录器;
    3. 如果处于严格模式,设置变量strict为true,否则,设置为false;
    4. 执行:GetIdentifierReference(env, name,strict):
      1. 如果env为null,返回一个表示解析失败的引用记录器。
      2. 调用env.HasBinding(name),查看name是否存在环境中:(
      3. 如果结果为true,返回一个表示该标识符的引用记录器(Reference Record);(
      4. 如果结果为false,通过env.[[OuterEnv]]获得外层环境记录器outer,然后递归地调用GetIdentifierReference(outer, name,strict);(

这里算法步骤后面的圆圈数字是我们需要重点关注的地方:

  • (注释①):这一步表示解析标识符时,会默认会选择执行上下文的词法环境作为起点。

    你或许会好奇,我们曾在8.执行环境说到,ECMAScript代码执行上下文比普通执行上下文,额外多出了三个组件:词法环境(LexicalEnvironment)、变量环境(VariableEnvironment)、私有环境(PrivateEnvironment)。但似乎在解析标识符时,只用到了这其中的一个。

    实际上,私有环境是用于绑定私有标识符的,私有标识符的解析会启用另外一套与标识符类似但不完全一样的逻辑 —— ResolvePrivateIdentifier。

    而变量环境也是曾经让我非常困惑的地方。在标准中,如果你进行一次全文检索,你只能看到变量环境被设置的过程,看不到变量环境被使用的过程。变量环境会被设置为绑定变量声明那一层的环境记录器,而这一层的环境记录器,是会在上面解析标识符的过程中被遍历到的,也就是说,即便没有变量环境这个概念,整个机制也不会出错误,变量环境看起来就仅仅起到了标记的作用。

    有同样困惑的不止我一个人,在标准的仓库中曾经有人就这个问题提了issue,但并未得到完全的解答。

  • (注释②):不同类型的环境记录器,HasBinding方法的逻辑可能不同:

    • 声明式环境记录器/函数环境记录器/模块环境记录器的HasBinding(N):查看是否有绑定标识符N,有的话返回true,没有的话返回false;
    • 对象环境记录器的HasBinding(N):查看环境记录器关联的对象是否有名为N的属性,有的话返回true,没有的话返回false;
    • 全局环境记录器的HasBinding(N):先调用[[DeclarativeRecord]]字段包含的声明式环境记录器的HasBinding(N)方法,如果返回false,再调用[[ObjectRecord]]字段包含的对象环境记录器的HasBinding(N)。这个逻辑也使得在全局环境中,会先查找词法声明(class/let/const)的变量,再查找变量声明(var/函数)的变量。
  • (注释③):引用记录器是一个记录器类型,常用于表示标识符的解析结果,它包含了与标识符相关的一系列信息:

    • [[ReferencedName]]字段:表示标识符的名字;
    • [[Base]]字段:标识符所在的环境记录器;
    • [[Strict]]字段:是否处于严格模式。

    得到的引用记录器,未来可以通过抽象操作GetValue获得标识符关联的值,并通过抽象操作PutValue修改这个值。许多其他的表达式,如赋值表达式,typeof运算符,它们都是基于引用记录器完成的。

  • (注释④):这一步表示递归查找标识符的过程,如果外层的环境记录器为null(即作用域链“到头”了),则表示查找失败,会返回一个[[Base]]字段为unresolvable的引用记录器,表示解析失败;


基于以上,在开篇的那个例子中,当要解析标识符i时,查找的链条如下图所示(黄色粗线):

open-pic-search

解析得到的结果是一个引用记录器:

Reference Record {[[Base]]: globalEnv.[[ObjectRecord]], [[ReferencedName]]: "i", [[Strict]]: false,[[ThisValue]]: empty
}

作用域链的构建

我们已经知道,不同类型代码在执行前会创建不同的环境记录器,然后通过声明实例化把这部分代码中的标识符绑定在环境记录器上;我们也已经知道,作用域链是通过这些环境记录器的[[OuterEnv]]字段串联起来的;于是,关于作用域链的构建,我们只缺少最后一块拼图了 —— [[OuterEnv]]字段的指向是如何决定的?

所有环境记录器,都会在创建的时候确定其[[OuterEnv]]字段,且这个字段在记录器整个生命周期内都不会发生改变。

于是,我们可以从不同类型环境记录器的创建算法以及这些算法的交叉索引找到[[OuterEnv]]指向的规律:

  • 创建全局环境记录器:[[OuterEnv]]永远为null,因此全局环境记录器是所有作用域链的最后一环;
  • 创建模块环境记录器:[[OuterEnv]]永远指向运行环境中的全局环境记录器;
  • 创建对象环境记录器:
    • 如果由with语句创建,[[OuterEnv]]设置为当前执行上下文的词法环境指向的环境记录器,并把执行上下文的词法环境更新为新创建的环境记录器;
    • 如果由全局环境记录器创建,则指向null;
  • 创建声明式环境记录器:如果由块级代码创建,[[OuterEnv]]设置为当前执行上下文的词法环境指向的环境记录器,并会把执行上下文的词法环境更新为新创建的环境记录器;如果由其他代码创建,则取决于其算法具体的逻辑。
  • 创建函数环境记录器:[[OuterEnv]]指向函数对象被创建时所处的环境记录器。

前面3种都比较简单,这里就不再解释。我们重点讲块级代码创建的声明式环境记录器以及函数代码创建的函数环境记录器。


块级代码

块级代码创建声明式环境记录器时,会把[[OuterEnv]]设置为当前执行上下文的词法环境指向的记录器,并把执行上下文的词法环境更新为新创建的记录器,而这个“动作”,我在9.作用域-块级声明实例化一节中已经为你展示过了,你也可以在块语句的求值语义中回顾所有的细节。

从中你也可以看出,执行上下文词法环境的指向是会随着代码的执行不断变化的。 比如,下面的全局代码会持续更新Script执行上下文中的词法环境:

// ①
var a = 1
{ // ②let b = 1{ // ③let c = 1}
}

下图为当代码执行到不同位置时(① - ② - ③),调用栈的模样。从这里你可以看到随着声明式环境记录器的创建,词法环境也在不断地更新。

block-env



函数代码

函数代码与块级代码则稍微有点不同,函数环境记录器的[[OuterEnv]]指向函数对象被创建时所处的环境记录器。

关于这部分内容,只要把环境可视化起来就会变得相当容易理解。我们来看看以下的代码:

// ①
let target = "global"
a()function a(){// ②let target = "fn_a"b()
}function b(){// ③console.log(target) // "global"
}

下图为当代码执行到不同位置时(① - ② - ③),调用栈的模样:

fn-env1

  1. 位置①:此时,script会进行全局声明实例化,函数a、b会在这个时候创建,并绑定在全局环境记录器中。

  2. 位置②:此时,准备执行函数a。函数a会创建函数环境记录器,由于函数a是在全局环境记录器中被创建的(全局环境记录器绑定了其标识符),所以函数环境记录器的[[OuterEnv]]会指向全局环境记录器。

  3. 位置③:此时,准备执行函数b。函数b会创建函数环境记录器,由于函数b是在全局环境记录器中被创建的,所以函数环境记录器的[[OuterEnv]]会指向全局环境记录器,而不是函数a的环境记录器,也因此最终输出的是gloabl而不是fn_a

这也就是为什么大家总说:函数的作用域链由函数声明的地方决定,而不是调用的地方决定。

实际上,函数对象在被创建的时候,会把当时所处的环境记录器,保存在函数对象上一个名为[[Environment]]的内部插槽中。当函数执行过程中使用抽象操作NewFunctionEnvironmnet创建函数环境记录器时,会直接从这个内部插槽取出先前保存的环境记录器,然后将新建的函数环境记录器的[[OuterEnv]]字段指向这个环境记录器,从而完成函数作用域链的构建。 如下图红色框部分所示。(关于“内部插槽”的概念,会在13.对象类型中介绍)

newFunctionEnv

在上面的环境图中你还可以看到另外一个细节,在执行到位置③时,环境中一共有3条作用域链,分别来自不同的ECMAScript代码执行上下文。每条作用域链从执行上下文词法环境开始,到全局环境记录器结束。于是我们也可以得出另外一个你没有在其他地方看见过的结论:调用栈内有多少个ECMAScript代码执行上下文,就会有多少条作用域链,尽管只有来自栈顶执行上下文的作用域链可以发挥实际作用。

关于这一点,我们也可以在chrome debugger中看到:

debugger

在Call Stack一栏,点击不同的执行上下文,就会在Scope中显示该执行上下文对应的作用域链。这里需要注意的是,Chrome debugger的Call Stack只会显示带有作用域链的ECMAScript代码执行上下文,所以这里只有3个栈元素,不包含全局代码执行上下文。其次,关于Scope的阅读方式,我已经在9.作用域中做了非常详细的解释,这里也不再啰嗦。


所谓闭包

有了上面的基础,再看“闭包”就觉得简单地不像话。

所谓的闭包,使用标准的术语,就是:尽管某段代码已经执行完毕,但是由这段代码创建的环境记录器,依然被这段代码中声明的某个函数保存在其[[Environemnt]]内部插槽里面,于是没有被销毁,且在函数未来执行的时候,该环境记录器依旧可以拿出来作为函数作用域链的一部分被使用。

让我们来看看以下的代码:

let x = 3;
// ①
let closure = outer(4);
closure(5);   function outer(x) {// ②return (y) => { // ③console.log(y + x);}
}

下图为当代码执行到不同位置时(① - ② - ③),调用栈的模样:

closure-env

  1. 位置①:此时正在执行全局代码,初始化了全局变量x为3。
  2. 位置②:此时,准备执行outer函数。他会先创建outer函数环境记录器,并在函数声明实例化的过程中,把参数x初始化为4。在后续执行return语句的过程中,return语句中的箭头函数表达式会创建箭头函数对象,并设置函数对象内部插槽[[Environment]]为outer函数环境记录器,最后返回这个函数对象。
  3. 位置③:此时,准备执行closure函数,由closure函数创建的函数环境记录器,其[[OuterEnv]]字段指向的是closure函数[[Environment]]内部插槽所保存的环境记录器,即outer函数环境记录器。因此,在解析标识符x时,得到的结果是outer函数环境记录器中的x(4),而不是全局变量x(3),即便此时outer函数早已执行完毕,其执行上下文也早已弹出调用栈并被销毁。

你要是觉得我的图不够清晰,那你也可以看看chrome debugger在位置③时给出的环境图:

debugger2


开篇的那段代码,最终输出了许多的5。在9.作用域中,我们使用let声明解决了这个问题,而这实际上也是一种“闭包”,只不过[[Environment]]内部插槽存的是一个由块级代码创建的声明式环境作用域:

for-bind-env1

你当然也可以刻意往[[Environment]]里面塞一个函数环境作用域,如下面的代码所示:

for (var i = 1; i < 5; i++) {setTimeout((function a(j){return () => {console.log(j) // ①}})(i), i * 1000)
}
// 1
// 2
// 3
// 4

此时,当第一次执行到位置①时,调用栈如下所示:

for-bind3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_274496.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Effective C++详细总结】第四章 设计与声明

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;C/C知识点 &#x1f4e3;专栏定位&#xff1a;整理一下 C 相关的知识点&#xff0c;供大家学习参考~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;…

Flink-转换算子

基本转换算子 map(映射) filter&#xff08;过滤&#xff09; flatMap&#xff08;扁平映射&#xff09; 聚合算子 keyBy&#xff08;按键分区&#xff09; 简单聚合 reduce&#xff08;归约聚合&#xff09; UDF介绍 函数类 富函数类 数据源读入数据之后&#xff0c;我们就可…

Neodynamic EPLPrinter SDK 2.0 for .NET Crack

Neodynamic EPLPrinter Emulator SDK for .NET Standard V2.0 添加对 FK&#xff08;删除表单&#xff09;、FR&#xff08;检索表单&#xff09;和 FS&#xff08;存储表单&#xff09;表单相关命令的支持。 21月 2023&#xff0c; 10 - 34&#xff1a;<>新版本 特征…

如何在24小时内让你的网站跻身谷歌前列?

在当今互联网时代&#xff0c;拥有一个排名靠前的网站对于企业来说非常重要&#xff0c;因为这意味着更多的流量和更高的曝光率。 而谷歌&#xff08;Google&#xff09;是全球最受欢迎的搜索引擎之一&#xff0c;因此在谷歌的搜索结果中排名靠前非常重要。 那么如何在24小时…

tomcat服务器前端部署【Tomcat Manager、思路分析】

问题描述 当前需要我进行前端代码的部署&#xff0c;但是我忘记了这个系统对应的部署位置&#xff0c;但是隐约记得好像是通过tomcat部署的。 然后当时为了方便部署&#xff0c;我们打开了Tomcat Manager 以下是基于Tomcat Manager的&#xff0c;没有打开的需要前往tomcat下载…

详解:企业知识管理的制作步骤!

随着信息技术的快速发展&#xff0c;企业面临着海量的信息和知识&#xff0c;如何管理和利用这些信息和知识&#xff0c;已经成为企业发展的重要问题。知识管理是一种管理方法和技术&#xff0c;旨在帮助企业有效地管理和利用知识资产&#xff0c;提高企业的创新能力和竞争力。…

【CSS】浮动 ② ( 浮动语法简介 | 文字环绕效果 | 左浮动 | 右浮动 )

文章目录一、浮动语法简介1、语法说明2、没有浮动的效果3、左浮动的效果4、右浮动的效果5、右浮动 外边距效果二、完整代码示例一、浮动语法简介 1、语法说明 为 元素 设置了 浮动 CSS 属性 , 可以实现 : 元素标签 不再受 标准流 控制 ; ( 块级元素 , 行内元素 , 行内块元素 …

【嵌入式Linux学习笔记】platform设备驱动和input子系统

对于Linux这种庞大的操作系统&#xff0c;代码重用性非常重要&#xff0c;所以需要有相关的机制来提升效率&#xff0c;去除重复无意义的代码&#xff0c;尤其是对于驱动程序&#xff0c;所以就有了platform和INPUT子系统这两种工作机制。 学习视频地址&#xff1a;【正点原子…

【JavaSE】泛型中的通配符

文章目录1. 概述2. 上界通配符 < ? extends E>3. 下界通配符 < ? super E>3. &#xff1f;和 T 的区别1. 概述 Java 泛型&#xff08;generics&#xff09;是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制&#xff0c;该机制允许开发者在编译时…

【QT神奇Bug】中文乱码、括号乱码、冒号乱码【2023.03.22】

&#x1f60d;Qt乱码疑难杂症解决方案 Solved by Yang Naifen. &#x1f4fa;视频讲解地址&#xff1a;【Qt疑难杂症之乱码-哔哩哔哩】 https://b23.tv/83MmXru 附言&#xff1a;解决这个bug按照我当前的薪资&#xff0c;至少四百RMB。都是工农阶级的工友&#xff0c;有bug一…

本地调试Java程序时只对部分接口忽略代理

场景 今天有位朋友问了个问题&#xff0c;在本地IDE开发工具调试代码的时候&#xff0c;怎么不动代码的情况只针对部分API走proxy&#xff0c;因为他们的代码只需要在本地调试的时候才要用到Proxy&#xff0c;而平时都是部署在云上&#xff0c;是用不到Proxy的&#xff0c;所以…

JDBC基础,介绍了简单的连接数据库,并通过在后端写SQL语句对数据库进行基本的增删查改操作

一、JDBC基础 跟数据库连接&#xff0c;并且可以对数据库里面的数据通过SQL语句进行处理等操作。 1.1 JDBC JDBC是SUN公司的&#xff0c;所以要按照他们的规范来&#xff0c;因为MYSQL和Oracle都是SUN公司的。三个产品都是一个公司的&#xff0c;一般不会出现兼容性不好的问…

智能手机2023:高端前攻、中端后守

配图来自Canva可画 沉寂许久的行业&#xff0c;终于在疫情之后迎来了久违的舞台&#xff0c;MWC线下展会三年来第一次召开。2月27日至3月2日&#xff0c;2023年世界移动通讯大会如期在巴塞罗那举行&#xff0c;国内一众手机厂商们纷纷登台亮相、大秀肌肉。与以往相比&#xff…

Rocketmq-Mqtt 开发实例

一、RocketMQ MQTT 概览传统的消息队列MQ主要应用于服务&#xff08;端&#xff09;之间的消息通信&#xff0c;比如电商领域的交易消息、支付消息、物流消息等等。然而在消息这个大类下&#xff0c;还有一个非常重要且常见的消息领域&#xff0c;即IoT类终端设备消息。近些年&…

Tomcat源码:启动类Bootstrap与Catalina的加载

参考资料&#xff1a; 《Tomcat源码解析系列&#xff08;一&#xff09;Bootstrap》 《Tomcat源码解析系列&#xff08;二&#xff09;Catalina》 《Tomcat - 启动过程&#xff1a;初始化和启动流程》 《Tomcat - 启动过程:类加载机制详解》 《Tomcat - 启动过程:Catalina…

不用科学上网,免费的GPT-4 IDE工具Cursor保姆级使用教程

大家好&#xff0c;我是可乐。 过去的一周&#xff0c;真是疯狂的一周。 GPT-4 震撼发布&#xff0c;拥有了多模态能力&#xff0c;不仅能和GPT3一样进行文字对话&#xff0c;还能读懂图片&#xff1b; 然后斯坦福大学发布 Alpaca 7 B&#xff0c;性能匹敌 GPT-3.5&#xff…

易基因:PIWI/piRNA在人癌症中的表观遗传调控机制(DNA甲基化+m6A+组蛋白修饰)|综述

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。2023年03月07日&#xff0c;南华大学衡阳医学院李二毛团队在《Molecular Cancer》杂志发表了题为“The epigenetic regulatory mechanism of PIWI/piRNAs in human cancers”的综述文章&am…

数据处理时代,绕不开的数据分析

数据分析的出现是因为人类难以理解海量数据所呈现出来的信息&#xff0c;不能从中找到相应的规律来对现实中的事物进行对应&#xff0c;我们都知道数据有很高的价值&#xff0c;但不能利用的价值&#xff0c;没有任何意义。 为了解决这一问题&#xff0c;数据分析在长期的数据…

Golang每日一练(leetDay0012)

目录 34. 查找元素首末位置 Find-first-and-last-position-of-element-in-sorted-array &#x1f31f;&#x1f31f; 35. 搜索插入位置 Search Insert Position &#x1f31f; 36. 有效的数独 Valid Sudoku &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 …

[vue问题]Uncaught SyntaxError: Not available in legacy mode

[vue问题]Uncaught SyntaxError: Not available in legacy mode问题描述问题分析解决方案直接回退vue-i18n的版本解决错误提示的问题问题描述 Uncaught SyntaxError: Not available in legacy modeat Object.createCompileError (message-compiler.cjs.js?af13:58:1)at creat…