js编译、执行上下文、作用域链

news2024/11/18 1:40:35

参考资料

极客时间课程《浏览器工作原理与实践》 – 李兵

《你不知道的JavaScript》-- Kyle Simpson

ES5.1规范:https://262.ecma-international.org/5.1/#sec-10.3

ES6规范:https://262.ecma-international.org/6.0/#sec-executable-code-and-execution-contexts

ES11规范:https://262.ecma-international.org/11.0/#sec-execution-contexts

掘金关于js执行上下文系列:https://juejin.cn/post/6844903741607395336#heading-0

掘金ES6规范下创建执行上下文和闭包的文章:https://juejin.cn/post/7024756814885421087/

在线工具:

查看js运行过程中的执行上下文:https://ui.dev/javascript-visualizer/

一、简述js编译

(一)常规编译流程

常规的编译流程主要有以下三个步骤:

  1. 分词/词法分析

    • 主要完成操作:将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单位。

    • 操作结果:生成词法单元流(数组)

//示例代码
var a = 2;

//分词结果:var	a	=	2	;	空格(是否分词出空格取决于在当前语言中,空格是否具有意义)
  1. 解析/语法分析

    • 主要完成操作:将词法单元流转换成一棵树,该树由元素逐级嵌套所组成,代表了程序语法结构,被称为“抽象语法树”。
    • 操作结果:生成抽象语法树(AST)
    • 代码示例如下图所示。推荐一个查看AST结构的网站:https://astexplorer.net/
      AST树结构示例
  2. 代码生成

    • 主要完成操作:将AST转换为可执行代码。略去其中的细节操作,可以简单理解为将AST转换为一组机器指令,用来分配内存、创建变量、存储值等。

    • 操作结果:生成可执行代码

(二)js编译

js的编译流程与常规编译流程一样也需要经过上述三个步骤,接下来我们对js在这三个步骤中所做的工作做大致了解。首先介绍一下js编译阶段出现的几个角色以及该角色的作用:

  • js引擎:从头到尾负责整个js代码的编译以及执行过程

  • 编译器:负责语法分析以及代码生成

  • 作用域:用于确定当前执行代码对标识符(变量)的访问权限,管理js引擎如何在当前作用域一级嵌套的子作用域中根据标识符名称进行变量查找。

列举示例来看上述三种角色在编译过程中如何合作:

//示例:
var a = 2;

//变量提升
var a = undefined;

//可执行代码
a = 2;
  1. 编译器进行词法分析、语法分析以及生成可执行代码。

    遇到变量声明时,首先询问作用域,当前作用域的集合中是否存在相同名称的变量?存在则忽略该变量声明,不存在则要求在当前作用域的集合中声明一个新的变量,并命名为a。

  2. js引擎执行可执行代码。

    询问作用域,当前作用域的集合中是否存在一个叫做a的变量,存在则直接对a进行赋值,不存在则进入当前作用域外层嵌套的作用域继续查找,直到找到该变量,或者抵达最外层作用域。

(三)从编译过程看作用域

在之前简单了解过在js中作用域分为:全局作用域、函数作用域、块级作用域(ES6定义),并没有深入了解作用域的定义。实际上作用域是一套根据名称查找变量的规则,用于帮助js引擎在编译、执行过程查找目标变量。这套规则的工作模型有两种:一种是词法作用域,一种是动态作用域。js采用的工作模型是词法作用域,动态作用域在《你不知道的JavaScript》-- Kyle Simpson的第一章中有介绍。

js采用的工作模型是词法作用域,它将根据代码的书写位置来确定作用域。在编译阶段的第一个步骤就是进行分词,在这个过程中,代码字符串被词法化(单词化)并分解。在词法化过程中可以根据代码的位置形成一个个词法作用域,这些作用域可能是全局的,也可能是属于某个函数的,也可能是属于某个代码块的,分别对应全局作用域、函数作用域、块级作用域。因此,词法作用域是定义在词法阶段的作用域,它由你在写代码时将变量、函数、块作用域写在哪里来决定的。特别地,函数的词法作用域与函数在何处被调用、如何被调用无关,只由函数被声明时所在的位置决定。下面列举代码分析词法作用域:

词法作用域

根据作用域之间的相互嵌套,可以分析出词法作用域链:bar函数作用域 --> foo函数作用域 --> 全局作用域。

作用域的作用范围在代码被书写的时候就已经确定了,是静态的;它区别于运行时的调用栈,调用栈是动态的,根据函数何时调用决定该函数上下文何时入栈。在ES5、ES6之后,作用域这一概念逐渐变为Lexical Environments(词法环境),与以下四个类型的代码结构相对应:

  • 上述全局作用域:对应一个词法环境

  • 上述函数作用域:对应一个词法环境

  • eval:进去eval调用的代码有时会创建一个新的词法作用域

  • with:一个with结构块内是一个词法环境

  • catch结构:一个catch结构块内也是一个词法环境

二、执行上下文的创建

在了解作用域链的形成之前,我们需要先了解执行上下文的创建。

(一)执行上下文

在深入研究执行上下文的具体结构时,需要注意好自己研究的是哪个版本的,在ES3中,执行上下文包含:scope(作用域)、Variable Object(变量对象)、ThisBinding;在ES5中,执行上下文中包含:变量环境(VariableEnvironment)、词法环境(LexicalEnvironment)、ThisBinding;在ES6以及ES6之后,执行上下文包含:变量环境(VariableEnvironment)与词法环境(LexicalEnvironment)。

在ES5,执行上下文有三个组成部分:

  • LexicalEnvironment:值是一个词法环境(Lexical Environment),基于代码词法嵌套结构来记录变量名/函数名与具体变量/函数的引用地址的关联,用来解析引用。
  • VariableEnvironment:变量环境,也是一个词法环境(Lexical Environment),用来登记var和function声明,ES6之后专门用于存储var声明的变量。一般,它在初始化的时候与LexicalEnvironment指向同一个词法环境。
  • ThisBinding:this值

注意:LexicalEnvironment只是名称,它的值是一个Lexical Environment(词法环境),为方便,之后使用LexicalEnvironment表示执行上下文中的LexicalEnvironment,使用词法环境表示Lexical Environment。在初始化执行上下文时,变量环境和LexicalEnvironment指向同一个词法环境,但在之后的运行过程中,LexicalEnvironment的值可能会被替换,VariableEnvironment的值则一直不变。

LexicalEnvironment和VariableEnvironment初始化时指向同一个词法环境,后来运行时什么情况下两者会指向不同的地址,以及出现块级作用域的情况下如何进行声明记录,这些不做深入探讨。以下只探讨ES5中,var、function声明如何被词法环境记录。

总结:ES5时,执行上下文 = 词法环境+ThisBinding,ES6及之后,执行上下文 = 词法环境。

(二)扩展了解—词法环境结构

在ES5中词法环境(Lexical Environments)的结构与ES6差不多,ES6之后的不同版本中,词法环境都有一定的差异,具体可翻阅规范文档进行深入了解,这里只对词法环境结构进行了解:

词法环境(Lexical Environments)由环境记录(Environment Record)和一个指向外部词法环境的引用(outer Lexical Environment)构成。其中环境记录记录所有在当前词法作用域内声明的变量、函数;outer则是作用域链能够连接起来的关键。环境记录可以认为是一个抽象类,它有三个具体的子类:声明环境记录、对象环境记录和全局环境记录,抽象类(指环境记录)定义了一些抽象规范方法,例如:判断是否有绑定、删除绑定等等规范方法,这些抽象方法在具体的子类中都有不同的具体算法实现。其中声明环境记录和对象环境记录是最常用的两种环境记录。
执行上下文
总结:词法环境主要由环境记录与外部词法环境的引用(outer)组成。

(三)分析执行上下文创建过程

在ES5.1规范中,通过以下代码示例分析执行上下文:

function foo() {
    var myName = "foo";
}
var myName = "window";
foo()
  1. 创建空的全局执行上下文并初始化,然后压入调用栈。全局执行上下文中outer的指向为null。
    执行上下文示例-初始化全局执行上下文
  2. 变量提升,在此过程对变量、函数声明进行注册与绑定。函数定义时创建函数对象,将外部词法环境存入内置属性[[Scope]],并在环境记录中将函数对象地址与函数名进行绑定。注意:函数声明定义的函数是在定义时创建函数对象,不同方式声明函数,函数被创建的时机、创建方式也有所不同,此处只以函数声明方式为例。

执行上下文示例-变量提升

  1. 为myName赋值,调用foo函数,编译函数体,创建新的词法环境。

  2. 将foo函数内置属性[[Scope]]的值赋值给词法环境的outer,这一步完成了将词法环境的outer指向外部词法环境。

  3. 为foo函数创建新的执行上下文,并进行变量提升。

执行上下文示例-创建函数执行上下文

  1. 函数执行完毕,foo函数执行上下文出栈
    执行上下文示例-函数上下文出栈

  2. 程序中不存在对内存中foo词法环境、map()的引用,它们将被回收。
    执行上下文示例-垃圾回收

  3. 代码执行完毕,全局执行上下文出栈,不存在对全局代码的词法环境、全局对象、函数对象的引用,它们将被回收。

总结:创建函数对象时将当前所在词法环境记录到[[Scope]],用来帮助设置函数执行上下文的词法环境的outer值。

三、作用域链

js执行代码过程中,查找变量时,会先从当前上下文中查找,如果没有找到,就会从外部作用域的执行上下文查找,一直找到全局上下文。这样由多个执行上下文构成的链表叫做作用域链。

(一) 作用域链的形成

根据执行上下文的创建,函数执行上下文的词法环境中,outer记录了外部词法环境的引用,而外部词法环境中的outer又记录了对其自身的外部词法环境的引用,由此形成了一个链式结构,这就是ES5.1规范中的作用域链。

而在ES6之后,作用域概念变为词法环境概念,函数对象的内置属性[[Scope]]变为[[Environment]],[[Environment]]记录了函数的外部词法环境。在函数对象被创建时,将所在的词法环境记录到[[Environment]],借此在函数被调用进而创建词法环境时,将[[Environment]]的值赋给outer,使得词法环境的outer指向外部词法环境,由此形成链式结构,这就是ES6之后的作用域链。

(二)示例代码分析作用域链查找

以ES5.1规范分析示例代码一:

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "foo";
    bar()
}
var myName = "window"
foo()

作用域链-示例代码1

如上图,绿色箭头线条分别是bar和foo的作用域链。执行bar函数,打印myName值时,先在自身词法作用域查找变量,找不到时顺着作用域链查找。最终在全局执行上下文的词法环境中找到变量myName。输出结果为:window。

四、闭包

在javascript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

分析示例代码1,可用工具网站查看执行上下文:

function foo() {
  var myName = "foo"
  function inner() {
    console.log(myName);
  }
  return inner;
}
var inner = foo()
inner()
  1. 创建全局词法环境,创建全局执行上下文,初始化上下文
    闭包示例-全局执行上下文

  2. 进行变量提升,将inner、foo函数记录到词法环境的环境记录中。为foo创建函数对象
    闭包示例1-全局代码变量提升

  3. 调用foo,开始编译foo函数体

    • 创建一个新的执行上下文,并使其成为运行执行上下文
    • 根据foo函数对象创建foo词法环境,将foo的[[Scope]]赋值给词法环境的outer
    • 将执行上下文的LexicalEnvironment和VariableEnvironment都指向foo词法环境
    • 将执行上下文压入调用栈
    • 变量提升时,创建环境记录,将词法环境中的Environment Record指向环境记录对象,将函数体内的声明的标识符记录在环境记录对象中。

闭包示例1-foo执行上下文

  1. 进行变量提升,对myName和inner函数提升:

    • 提升inner函数时,快速扫描inner函数体,以确定inner函数中是否使用了自由变量(可以简单理解为外部变量)

      • 存在自由变量则创建闭包

        • 将自由变量与foo词法环境的环境记录进行匹配,将匹配到的变量放入闭包中,同时将匹配到的变量在环境记录中清除
        • 将闭包对象的内存地址推入环境记录
      • 不存在自由变量则不创建闭包

    • 为inner创建函数对象

闭包示例1-创建闭包

  1. 执行foo函数可执行代码,将inner返回给全局变量inner。foo函数执行上下文出栈并被销毁。

闭包示例1-foo出栈
6. 如上图所示,foo执行上下文出栈并被销毁,foo的词法环境本应被销毁。根据图中1、2所示,由于全局词法环境中存在着对foo词法环境的间接引用,使得foo的词法环境没有被销毁。

  1. 调用inner函数,开始编译inner函数体

  2. 执行inner函数体,查找变量myName:

    • 先在inner函数的词法环境中查找myName,找不到,根据outer进入外部词法环境
    • 进入foo词法环境中查找,在闭包中找到myName,查找结束
  3. 最终输出结果:foo

从词法环境角度来说,闭包是函数和声明该函数的词法环境的组合。有说法,在js中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。关于是否所有js函数都是闭包的讨论,可以参考:https://segmentfault.com/a/1190000015311755?utm_source=tag-newest。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis 五大基本数据类型常见命令

一、redis中的常见数据结构 Redis共有5种常见数据结构,分别字符串(STRING)、列表(LIST)、集合(SET)、散列(HASH)、有序集合(ZSET)。 二、redis中字符串(String)介绍 String 类型是…

小红书内容种草怎么玩,koc铺量原则

一直以来,小红书平台都以其强大的种草力,而备受品牌关注。许多初创品牌更是将平台看做抢占市场的前沿阵地。那么小红书内容种草怎么玩,koc铺量原则是什么呢?今天来为大家详细介绍一下。 一、什么是koc铺量 要做好新品预热的koc铺量&#xff…

存bean和取bean

准备工作存bean获取bean三种方式 准备工作 bean:一个对象在多个地方使用。 spring和spring boot:spring和spring boot项目;spring相当于老版本 spring boot本质还是spring项目;为了方便spring项目的搭建;操作起来更加简单 spring…

有效和无效的帮助中心区别在哪?如何设计有效的帮助中心?

帮助中心就是一个丰富的知识库,可以对企业的潜在客户进行引导。不仅能够提升用户的使用体验还能为企业塑造更加专业的品牌形象,在使用过程中为用户提供帮助。帮助中心的目的就是为了解决用户在使用过程中遇到的困难,同时为用户的使用提供引导…

DC-9通关详解

信息收集 漏洞发现 result.php处存在sql注入 sqlmap跑信息 python sqlmap.py -u http://192.168.45.146/results.php --data search1 -D users -T UserDetails --dump 拿了几个尝试登录都无效 ssh尝试登录直接拒绝了 再看Staff表 查哈希 进后台 多了一个添加记录的功能 没啥…

数据结构(二叉树)

文章目录 一、树的基础概念1.1 树型结构1.2 树型的概念 二、二叉树2.1 概念 性质2.2 二叉树的存储2.2 二叉树的基本操作(1)遍历(2)其他 2.3 二叉树练习 一、树的基础概念 1.1 树型结构 树是一种非线性的数据结构,它…

ChatGPT 由0到1接入 Siri

ChatGPT 由0到1接入 Siri ChatGPT 由0到1接入 Siri第一步:获取 OpenAPI 的 Key第二步:制作快捷指令本教程收集于: AIGC从入门到精通教程 ChatGPT 由0到1接入 Siri 分享如何将 GPT 应用集成到苹果手机的 Siri 中 (当然手机是需要魔法(TZ)的) 第一步:获取 OpenAPI 的…

网络安全可以从事哪些岗位?岗位职责是什么?

伴随着社会的发展,网络安全被列为国家安全战略的一部分,因此越来越多的行业开始迫切需要网安人员,也有不少人转行学习网络安全。那么网络安全可以从事哪些岗位?岗位职责是什么?相信很多人都不太了解,我们一起来看看吧。 1、安全…

电阻阻值读取方法、电容容值的读取方法

电阻、电容的数值读取方法 文章目录 电阻、电容的数值读取方法前言1、电阻读数1.1 贴片电阻1.2.直插色环电阻 2、电容读数2.1 电容单位换算2.2 电容读数方法 前言 现在随着电子产品的不断升级优化,做到体积越来越小了,以前发现还是用得很多直插电阻和一…

百年不用了,今天拾起来 sort() 排序

简单赘述一下需求。 原本前端调用后端接口是自带排序功能的,一般是按照创建单据的时间,后端会处理好返回给我们。 但是有时候有特别的限制,需要前端自行处理排序展示。 如上图所示, 列表和列表扩展行均要根据我们新增或编辑的时候…

ATTCK v13版本战术介绍——防御规避(五)

一、引言 在前几期文章中我们介绍了ATT&CK中侦察、资源开发、初始访问、执行、持久化、提权战术理论知识及实战研究、部分防御规避战术,本期我们为大家介绍ATT&CK 14项战术中防御规避战术第25-30种子技术,后续会介绍防御规避其他子技术&#xf…

什么蓝牙耳机好?业内权威蓝牙耳机排名TOP5

蓝牙耳机是当下最热门的数码产品,我个人已经买过十来款蓝牙耳机了,最近逛论坛看到知名数码论坛公布了蓝牙耳机排名TOP5,不懂什么蓝牙耳机好的朋友们在选购时可以从中入围的品牌中进行挑选。 一、JEET Air2蓝牙耳机 推荐理由:舒适…

springboot项目部署教程【本地+云服务器】

目录 前言一、环境准备二、项目导入三、配置Maven四、数据库导入五、启动项目六、浏览器访问结语 前言 springboot项目部署教程用最简单、暴力的方法完成项目导入。 🥇个人主页:MIKE笔记 🥈文章专栏:毕业设计源码合集 ⛄联系博主…

一、LLC 谐振变换器工作原理分析

1 前言 LLC 谐振电路采用脉冲频率调制(PFM),通过改变驱动信号的频率来控制变换器的能量传输。谐振电路中的三个谐振元件为:谐振电感 Lr、谐振电容 Cr 和励磁电感 Lm,它们根据工作模式的不同可形成两个谐振频率。与串联谐振变换器相比&#x…

vue 组件 隐藏内容,点击展示更多功能

效果图 代码 <template><div class"m-text-overflow modules"><div class"l-content" :style"contentStyle"><div ref"refContent"><slot><span v-html"content"> </span></…

基于AT89C52单片机的万年历设计与仿真

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/87777668?spm1001.2014.3001.5503 源码获取 主要内容&#xff1a; 本次设计所提出的一种基于单片机技术的万年历的方案&#xff0c;能更好的解决万年历显示的问题…

公司招人,面试了一个4年经验要20K的,一问自动化都不会····

公司前段时间缺人&#xff0c;也面了不少测试&#xff0c;结果竟然没有一个合适的。一开始瞄准的就是中级的水准&#xff0c;也没指望来大牛&#xff0c;提供的薪资在10-20k&#xff0c;面试的人很多&#xff0c;但平均水平很让人失望。 看简历很多都是3、4年工作经验&#xf…

【腾讯云 Finops Crane集训营】Finops Crane究竟能为我们带来什么价值和思考?深入探究Crane

目录 前言 一、Crane目的是什么&#xff1f; 二、Crane有哪些功能&#xff1f; 1.成本可视化和优化评估 2.推荐框架 3.基于预测的水平弹性器 4.负载感知的调度器 5.拓扑感知的调度器 6.基于 QOS 的混部 三.Crane的整体架构及特性 1.Crane架构 Craned Fadvisor Metr…

postman runner使用外部数据

场景: 使用postman进行接口测试&#xff0c;需要对一个collection中的所有接口进行测试&#xff0c;或者需要使用指定的参数对collection中的接口进行测试。 工具&#xff1a; Postman for Windows Version 10.12.0接口文件&#xff08;链接&#xff1a;https://pan.baidu.co…

环境土壤物理模型HYDRUS建模方法

查看原文>>>系统学习环境土壤物理模型HYDRUS建模方法与多案例应用 目录 一、HYDRUS模型概述 二、土壤和地下水流问题基础知识 三、 溶质运移问题模拟 四、热量传输问题模拟 五、模型外部接口 其它生态环境相关推荐 HYDRUS是由著名土壤学家Rien van Genuchten和…