深入理解JavaScript——执行上下文与调用栈

news2024/11/19 16:46:48

前言

在说一个概念前,我们需要确定它的前提,此文以 ECMAScript5 为基础撰写

一句话解释

执行上下文就是一段代码执行时所带的所有信息

执行上下文是什么

《重学前端》的作者 winter 曾经对什么是执行上下文做过这样的解释:

JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文

并且他整理出在不同 ECMAScript 版本中执行上下文所代表的含义:

执行上下文在 ES3 中,包含三个部分。

  • scope:作用域,也常常被叫做作用域链
  • variable object:变量对象,用来存储变量的对象
  • this value: this 值

在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改成下面这个样子

  • lexical environment:词法环境,当获取变量时使用
  • variable environment:变量环境,当声明变量时使用
  • this value: this 值

在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容

  • lexical environment:词法环境,当获取变量或者 this 值时使用
  • variable environment:变量环境,当声明变量时使用
  • code evaluation state: 用于恢复代码执行位置
  • Function:执行的任务是函数时使用,表示正在被执行的函数
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码
  • Realm:使用的基础库和内置对象实力
  • Generator:仅生成器上下文有这个属性,表示当前生成器

总结的很完整,按照 选新不选旧 原则,本文应该以 ES2022 为切入点展开,最次也要 ES2018,但主流的解释执行上下文都以 ES3/ES5 为例,权衡之后,笔者将以 ES5 为基础撰写执行上下文,并在后续补充说明 ES3 中的执行上下文

执行生命周期

我们在讲 词法环境 时,曾经画过一张执行生命周期图,当时所讲的词法环境是在**(预)编译阶段产生,现在讲的执行上下文是在引擎执行阶段**进行

一段代码如果要执行,首先会往调用栈(call stack)中压入全局执行上下文;再创建词法环境,此时变量该提升提升,函数该提升提升,并将这些变量登记到词法环境中(编译阶段);接着进入执行阶段,执行可执行代码,该赋值赋值,遇到函数,就创建一个函数执行上下文,并往调用栈中压入该函数的执行上下文;而后创建该函数的词法环境,当该函数执行完后,从调用栈中弹出;反复循环,到最后调用栈中只剩一个全局执行上下文,除非你关闭浏览器,不然全局执行上下文不会弹出

我们要往调用栈中压入执行上下文,调用栈的数据结构为 。特点为先进后出

如果我们的代码是这样的

var a = 1;

function foo() {function bar() {console.log(a);}bar();
}

function baz() {foo();
}

baz(); 

那么执行过程应该是这样:

图中的蓝色方块为 执行上下文 ,外面黑框白底的区域就是模拟 调用栈。整个过程遵循先进后出的原则

  • 在任何代码执行之前,先创建全局执行上下文,并往调用栈中压栈(编译阶段)
  • 创建词法环境,登记函数声明和变量声明(编译阶段)
  • 引擎执行到 baz() ,创建 baz() 的函数执行上下文,并往调用栈中压栈
  • 函数 baz() 调用 foo() ,创建 foo() 执行上下文,并将其压入调用栈中,
  • 函数 foo() 调用 bar() ,创建 bar() 执行上下文,并将其压入调用栈,
  • 函数 bar() 执行 console.log() ,同理将其压入调用栈,
  • 执行完 console.log() 后,被弹出
  • 函数 bar() 执行完毕,弹出调用栈
  • 函数 foo() 也执行完毕,弹出调用栈
  • 函数 baz() 同样执行完毕,弹出调用栈

只剩下全局执行上下文,留在栈底

在这里,我们看到 console.log() 也被压入到执行栈中,不禁有个思考,哪些代码元素会被执行到调用栈中呢?

可执行代码

事实上,不仅仅是 function 可以作为执行上下文在执行栈中运行,在 JavaScript 里定义了四种可执行代码:

  • global code:整个 js 文件
  • function code:函数代码
  • module:模块代码
  • eval code:放在 eval 的代码

所以才会看到 console.log() 被压入调用栈中,因为它属于 global code

执行步骤

JavaScript 引擎是按照可执行代码来执行代码的,每次执行步骤如下:

1.创建一个新的执行上下文(Execution Context)
2.创建一个新的词法环境(Lexical Environment)
3.把 LexicalEnvironment 和 VariableEnvironment 指向新创建的词法环境
4.把这个执行上下文压入执行栈并成为正在运行的执行上下文
5.执行代码
6.执行结束后,把这个执行上下文弹出执行栈

如何创建执行上下文

到现在,我们已经知道 JavaScript 是如何管理执行上下文的,现在让我们了解一下 JavaScript 引擎是怎样创建执行上下文的

创建执行上下文有两个阶段:1) 创建阶段2) 执行阶段

创建阶段

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生以下三件事:

  • this 值的确定,即我们所熟知的 this 绑定。* 创建词法环境组件(LexicalEnvironment component )* 创建**变量环境组件(VariableEnvironment component )**所以执行上下文在概念上表示如下:
ExecutionContext = {ThisBinding = <this value>,LexicalEnvironment = { ... },VariableEnvironment = { ... },
} 

这里需要再多嘴一句:

在很多文章中我们看到执行上下文只有 LexicalEnvironment 和 VariableEnvironment ,并没有 this。那是因为在 ES2018 后,this 就归纳到 LexicalEnvironment (如上文 winter 所说),但本文是以 ES5 为基础撰写,故此版本的执行上下文中是有 this 的

this 绑定

this 的指向很简单,谁调用我,我只想谁

在执行上下文中,this 就指向那个调用者

词法环境组件 和 变量环境组件

这两”姐妹“有点像,只是分工不同。

变量环境组件(VariableEnvironment component ) 用来登记 varfunction 等变量声明

词法环境组件(LexicalEnvironment component ) 用来登记 letconstclass 等变量声明

按上例可以这么画图:

LexicalEnvironmentVariableEnvironment 则都是词法环境(Lexical Environment)。很多文章中常把 LexicalEnvironment 理解成 词法环境,这是不对的,LexicalEnvironment 是一个单词,表示执行上下文中的是标识 letconstclass 等变量声明,而 VariableEnvironment 则是标识 varfunction 等变量声明

如果非要用中文来表示 LexicalEnvironment 的话,我更愿意用 词法环境组件 来表示;同理,VariableEnvironment 则用 变量环境组件 来表示

对我而言,变量环境组件和词法环境组件就好比”装黄豆瓶“和”装绿豆瓶“,一个负责装黄豆(var,function),一个负责装绿豆(let,const,class),它们指向词法环境,本质是从词法环境中拿数据

所以无论是词法环境组件还是变量环境组件,都有一个环境记录器和一个 outer 对象,其中环境记录器记录变量,outer 指向父级作用域

具体可以看 ECMAScript 6 标准中的第八节 详细了解一下

回头看上述例子:

bar() 函数中的 console.log(a) ,本环境记录器中找不到,就引着 outer 找,在 window 中找到变量 a ,赋值后,弹出,foo() 执行完后也弹出,baz() 执行完弹出,留下全局执行上下文在栈底

之前在说 词法环境 时,我们曾下过这样的定义:outer 就是指向词法环境的父级词法环境(作用域)

但是这里就有个疑惑了,outer 既然指词法环境的父级作用域,那作用域链从那里来?

以上面的 demo 为例,bar 的执行上下文的伪代码:

BarExecutionContext = {ThisBinding = <Global Object>,LexicalEnvironment = { 		EnvironmentRecord: { ... },outer: <FooLexicalEnvironment>
	},VariableEnvironment = {EnvironmentRecord: { ... },outer: <FooLexicalEnvironment>},
} 

bar 的 outer 指向 foo 执行上下文, foo 的 outer 指向 window,变量先从当前执行作用域查找变量,如果找不到,就引着 outer 继续查找。这一过程中,bar 作用域—foo 作用域—window 作用域。

当代码要访问一个变量时——首先会搜索当前词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境

而我们潜意识中的”作用域链“是在 ES3 中的才有的,因为那个时候的执行上下文中有 scope(作用域),当前作用域找不到变量,就往外层作用域中找,然后再到更外层的作用域找,知道全局作用域,作用域与作用域之间以链表的形式连接着,这就是作用域链

执行上下文有几种

分三种

  • 全局执行上下文* 函数执行上下文* eval 执行上下文ECMAScript3 中的执行上下文

如果你喜欢看有关执行上下文的文章,应该常看到这样的描述,执行上下文由变量对象(Variable object,VO)、作用域链(Scope chain) 、this 构成。

其实我们可以把变量对象(VO) 看成是 ES5 中的词法环境,scope 为词法环境中的 outer

如何追踪执行上下文栈

例如:

function foo1() {foo2();
}
function foo2() {foo3();
}
function foo3() {foo4();
}
function foo4() {console.lg('foo4');
}
foo1(); 

得到错误提示如图:

或者在 Chrome 中执行代码,打断点得到:

关于变量对象与活动对象

笔者最开始了解执行上下文、执行上下文栈以及闭包时,大家是用变量对象和活动对象来解释的,这是 ES3 时的词汇,而本文是以 ES5 为标准展开

其两者的涵义简单来说,变量对象(Variable object)是与执行上下文相关的对象,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object,AO)来表示变量对象

作用域在(预)编译阶段确定,但是作用域链是在执行上下文的创建阶段完成生产的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向

总结

执行上下文可以理解为函数的执行环境,当函数执行时,都会创建一个执行环境

每次只能有一个执行上下文处于运行状态,因为 JavaScript 是单线程语言,它由执行栈或(叫)调用栈来管理

创建一个函数,就生成了一个作用域;调用一个函数,就生成一个作用域链

执行上下文创建阶段分为绑定 this,创建词法环境,变量环境三步

调用函数时,创建一个新的词法环境

词法环境这个说法,是 ES5 规范中的内容,可以理解为 ES3 中的变量对象,scope 为 词法环境中的 outer

在 ES5 中,词法环境变量环境 的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定

在 ES3 时,执行上下文包括了变量对象、作用域链以及 this

在 ES5 时,执行上下文则包括词法环境、变量环境、this。其中词法环境或者变量环境都是由用于环境记录器和 outer 对象组成,其中 outer 指向父级作用域,环境记录器记录自由变量

Q&A

Q:是不是说在定义时确认了作用域,在调用时确认了作用域链?

A: yes,这里需要注意的是每一个执行上下文都会进行提升操作

Q: ES5 中的执行上下文的词法环境和编译阶段的词法环境有什么不同?

A:一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进行执行阶段,大致流程如下:

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

基于基于全局差错能量函数的双目图像立体匹配算法matlab仿真,并提取图像的深度信息

目录 1.算法概述 2.仿真效果预览 3.核心MATLAB代码预览 4.完整MATLAB程序 1.算法概述 全局的能量函数公式如下: E(f)Edata(f)Esmooth(f) 其中,Edata 表示能量函数的数据项,意为该像素只考虑自身的视差值的倾向,不考虑 邻域内其他像素的影响;N 表示匹配聚合时的支持窗口;p 表…

应用层-HTTP协议

HTTP概述 HTTP(HyperTextTransferProtocol)是Web应用的应用层协议&#xff0c;定义浏览器如何向Web服务器发送请求以及Web服务器如何向浏览器进行响应。目前主要使用的HTTP/1.0 和HTTP/1.1&#xff0c;尤其以HTTP/1.1 为主流。 HTTP连接 浏览器在向服务器发送请求之前&#…

全日制和非全日制之争,看完六年前的这个文件心里就有数了

在每年的报考咨询中&#xff0c;都能接触到不少关于非全日制硕士的质疑&#xff0c;最大的争议点无非在于社会含金量的问题。其实很多年以前是没有非全日制这一说法的&#xff0c;早些年很多学员也是在职周末读的双证MBA/MPA/MEM这些专业&#xff0c;但证书一律是全日制标识&am…

Oracle Primavera Unifier进度管理器(Schedule Manager)

目录 功能介绍 功能包括 功能介绍 在进度管理器中&#xff0c;Primavera Unifier 用户可以在项目/外壳和项目群级别创建和管理进度表。他们可以创建根据项目或外壳的需求自定义的项目/外壳计划表。当他们为项目/外壳创建第一个时间表表时&#xff0c;Primavera Unifier 会自…

【软考】系统集成项目管理工程师(九)项目成本管理

这里写目录标题 一、项目成本管理概述二、项目成本管理子过程1. 规划成本2. 成本估算3. 制定预算4. 控制成本一、项目成本管理概述 成本 即项目的全过程中所耗用的各种成本,它们的总和为项目成本。成本管理 是在预算范围内确保项目团队完成一个项目所需要开展的管理过程,项目…

前端网页项目-学成在线案例

典型的企业级网站目的是为了整体感知企业级网站布局流程&#xff0c;复习以前知识 准备素材和工具&#xff1a; 学成在线PSD源文件开发工具PS&#xff08;切图&#xff09;/cutterman插件vscode&#xff08;代码&#xff09;chrome&#xff08;测试&#xff09; 案例准备工作&…

[附源码]java毕业设计健身健康规划系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

发布 .NET 7 MAUI / MAUI Blazor 应用到 Windows 应用商店

.NET MAUI 目前仅允许发布 MSIX 包。 原文地址 https://www.cnblogs.com/densen2014/p/16885318.html 创建签名证书发布到本地传送门 https://www.cnblogs.com/densen2014/p/16567384.html 使用 Visual Studio 2022 发布到 Windows 应用商店 由于是发布到 Windows 应用商店,本…

代码中可能会使用

代码中可能会使用 日志 结合兼具举报系统日志 https://www.cnblogs.com/lingduqianli/p/7589173.html 拦截器 https://blog.csdn.net/neymar_jr/article/details/79115839 拦截器应用场景 拦截器本质上是面向切面编程&#xff08;AOP&#xff09;&#xff0c;符合横切关注点…

MYSQL窗口函数(Rows Range)——滑动窗口函数用法

语法介绍 窗口函数语法&#xff1a; <窗口函数> over (partition by <用于分组的列名> order by <用于排序的列名> rows/range子句<用于定义窗口大小> ) <窗口函数>可以放以下两种函数&#xff1a; 1&#xff09; 专用窗口函数&#xff0c;包括…

图文详解Linux基础经典教程(07)——CentOS安装Tomcat

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 概述 之前&#xff0c;我们在CentOS中安装了JDK&#xff1b;接下来&#xff0c;我们在CentOS中安装Tomcat。 安装步骤 在此&#xff0c;详细介绍Tomcat的安装步骤。 第一…

上海亚商投顾:A股缩量调整 AIGC、Web3.0概念抢眼

上海亚商投顾前言&#xff1a;无惧大盘大跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪三大指数今日震荡调整&#xff0c;深成指、创业板指午后均跌超1%&#xff0c;黄白二线有所分化&#xff0c;科创50指…

Django Celery异步任务队列

“ https://github.com/celery/celery” celery的GitHub源码文件 Celery是一个异步任务队列&#xff0c;需要python的环境&#xff0c;一般可用于python的web开发框架“食”用&#xff0c;例如Django。 场景 例如在自己开发网站时&#xff0c;写发送短信验证码的部分&#xff0…

Http不转换成Https会有什么后果?

自从互联网诞生以来&#xff0c;大家一开始接触的就是http站点&#xff0c;类似于http://域名&#xff0c;看习惯了也用习惯了。不过&#xff0c;随着人们网络安全意识的提高&#xff0c;越来越多的http站点已经被https站点所替代&#xff0c;逐年在增加。那么&#xff0c;http…

艾美捷C1q天然蛋白化学性质和相关研究方案

艾美捷C1q天然蛋白背景&#xff1a; C1q是补体系统C1的组成成份。它是一个巨分子量 (460kD) 糖蛋白。一个C1q分子由18条多肤链组成,含A、B、C 3条不同链。补体系统通过经典途径、旁路途经和甘露糖结合凝集素途径激活&#xff0c;而C1q是补体经典途径重要的启动分子&#xff0c…

【干货】STM32通过ADC模拟看门狗实现掉电保存

1.前言 很多时候我们需要将程序中的一些参数、数据等存储在EEPROM或者Flash中&#xff0c;达到掉电保存的目的。但有些情况下&#xff0c;程序需要频繁的修改这些参数&#xff0c;如果每次修改参数都进行一次保存&#xff0c;那将大大降低存储器的寿命。尤其是单片机内部Flash&…

uniapp中使用微信小程序custom-tab-bar

uniapp中使用微信小程序custom-tab-bar1、配置信息2、添加 tabBar 代码文件3、 编写 tabBar 代码4、踩坑5、解决今天把小程序项目中的 tabBar 改为使用自定义的tabbar 其实很简单&#xff0c;只要按照小程序开发文档中的步骤来做 1、配置信息 在 app.json 中的 tabBar 项指定…

移动魔百盒UNT401A、UNT403A、UNT413A_晶晨S905L3A/B芯片_红外蓝牙语音_免拆卡刷固件

移动魔百盒UNT401A、UNT403A、UNT413A_晶晨S905L3A/B芯片_红外蓝牙语音_免拆卡刷固件&#xff0c;28G或216G配置-安卓9.0 支持最新出UWE5621DS/MT7661/MT7663/MT7668/RTL8822CS及以往其他无线型号-当贝红外蓝牙语音免拆卡刷包固件。 固件特点&#xff1a; 1、修改dns&#xf…

使用EasyCV Mask2Former轻松实现图像分割

作者&#xff1a;贺弘 谦言 临在 导言 图像分割(Image Segmentation)是指对图片进行像素级的分类&#xff0c;根据分类粒度的不同可以分为语义分割(Semantic Segmentation)、实例分割(Instance Segmentation)、全景分割(Panoptic Segmentation)三类。图像分割是计算机视觉中的…

CDD文件——CANdelaStudio

诊断协议那些事儿 本文为诊断协议那些事儿专栏文章&#xff0c;在CANoe诊断测试一文中介绍了导入CDD文件&#xff0c;CDD(CANdela Diagnostic Descriptions&#xff0c;CANdela诊断描述)是诊断数据的数据库&#xff0c;与CAN消息的数据库文件DBC类似&#xff0c;也就是上文提到…