当前位置: 首页 > Web前端 > HTML

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

时间:2023-03-29 11:29:58 HTML

前言在谈一个概念之前,我们需要先确定它的前提。本文基于ECMAScript5,写了一句话解释,执行上下文就是一段代码执行的所有信息。执行什么是上下文?《重学前端》的作者Winter曾经解释过什么是执行上下文:JavaScript标准将一段代码(包括函数)执行所需的所有信息定义为:“执行上下文,他整理了不同ECMAScript中的执行上下文执行上下文在版本中的含义:执行上下文在ES3中包含三部分。scope:作用域,也常被称为作用域链variableobject:变量对象,用来存储变量的对象thisvalue:thisvalue在ES5中,我们改进了命名方式,将执行上下文的前三部分改成如下样子thislexicalenvironment:词法环境,获取变量时使用variableenvironment:variableenvironment,声明变量时使用thisvalue:thisvalue在ES2018中,执行上下文又变成这样了,thisvalue归类到lexicalenvironment,但是内容很多已添加。lexicalenvironment:词法环境,获取变量或this值时使用变量环境variableenvironment:变量环境,声明变量时使用codeevaluationstate:用于还原代码执行位置Function:当执行的任务是函数时使用,表示函数beingexecutedScriptOrModule:当被执行的任务是脚本或模块时使用,表示正在执行的代码根据选新不选旧的原则,本文应该从ES2022开始,最后是ES2018,但是主流解释和执行上下文都以ES3/ES5为例。经过权衡,笔者将以ES5为基础编写执行上下文,后续在ES3中加入执行上下文执行生命周期。我们在讲词法环境的时候,曾经画过一个执行生命周期图。提到的执行上下文是指如果在引擎执行阶段要执行一段代码,会先将全局执行上下文压入调用栈(callstack);然后创造一个词法环境。这时要提升变量,提升函数,并将这些变量注册到词法环境中(编译阶段);然后进入执行阶段,执行可执行代码,赋值,遇到函数时,创建函数执行上下文,将函数的执行上下文压入调用栈;然后创建函数的词法环境,当函数执行完毕,将从调用栈中弹出;如此循环,直到调用栈中只剩下一个全局执行上下文,除非关闭浏览器,否则全局执行上下文不会弹出我们要将执行上下文压入调用栈,调用的数据结构堆栈是一个堆栈。如果我们的代码像这样,功能是先进后出的vara=1;functionfoo(){functionbar(){console.log(a);}bar();}functionbaz(){foo();}baz();那么执行过程应该是这样的:图中蓝色方块就是执行上下文,外面黑框白底的区域就是模拟的调用栈。整个过程遵循先进后出的原则。任何代码执行前,先创建全局执行上下文,压入调用栈(编译阶段)创建词法环境,注册函数声明和变量声明(编译阶段),引擎执行到baz(),创建baz()的函数执行上下文,并将其压入调用堆栈。函数baz()调用foo(),创建foo()的执行上下文,并将其压入调用堆栈。),创建bar()执行上下文,并将其压入调用堆栈。函数bar()执行console.log(),并以相同的方式将其压入调用堆栈。执行console.log()后,弹出函数bar(),同时执行弹出调用栈函数foo(),同时执行弹出调用栈函数baz(),pop-upcallstack只剩下globalexecutioncontext,这里保留在栈底,我们看到console.log()也被压入了执行栈,不禁想到,会是哪些代码元素执行到调用栈?可执行代码其实不仅仅是function可以作为执行上下文在执行栈中运行,JavaScript中定义了四种可执行代码:globalcode:整个js文件functioncode:functioncodemodule:模块代码evalcode:put在eval的代码中,你会看到console.log()被压入了调用栈,因为它属于全局代码执行步骤。JavaScript引擎根据可执行代码执行代码。每次执行的步骤如下:CreateanewExecutionContext创建一个新的词法环境(LexicalEnvironment),将LexicalEnvironment和VariableEnvironment指向新创建的词法环境,将这个执行上下文压入执行栈,成为正在运行的执行上下文。上下文弹出执行栈如何创建执行上下文?现在我们知道JavaScript如何管理执行上下文,现在让我们了解JavaScript引擎如何创建执行上下文。创建执行上下文有两个阶段:1)创建阶段和2)执行阶段创建阶段在JavaScript代码执行之前,执行上下文会经历创建阶段。在创建阶段会发生以下三件事:this值的确定,称为this绑定。创建一个LexicalEnvironment组件,并创建一个VariableEnvironment组件,因此执行上下文在概念上表示如下:ExecutionContext={ThisBinding=,LexicalEnvironment={...},VariableEnvironment={...},}这里需要:在很多文章中,我们看到执行上下文只有LexicalEnvironment和VariableEnvironment,并没有this。那是因为在ES2018之后,this被归纳为LexicalEnvironment(上面winter说的),但是这篇文章是在ES5的基础上写的,所以在这个版本的执行上下文中,有this指向this的绑定就很简单了,whocallsMe,我只想谁在执行上下文中,this指向调用者。词法环境组件和变量环境组件这两个“姐妹”有点相似,只是分工不同。变量环境组件(VariableEnvironmentcomponent)用于注册var、function等变量声明。LexicalEnvironment组件(LexicalEnvironment组件)用于注册let、const、class等变量声明。根据上面的例子,你可以这样画一个图:LexicalEnvironment和VariableEnvironment都是词法环境(LexicalEnvironment)。在很多文章中,LexicalEnvironment常被理解为词法环境。这是错误的。LexicalEnvironment是一个词,意思是在执行上下文中,标识了let、const、class等变量声明,而VariableEnvironment是标识了var、function等的变量声明。如果需要的话我用中文表达LexicalEnvironment,我更喜欢用词法环境成分来表达;类似地,VariableEnvironment使用变量环境组件来表达。对我来说,变量环境组件和词法环境组件就像是“打包大豆瓶”和“打包绿豆瓶”,一个负责加载大豆(var,function),一个负责加载绿豆(let,const,class)。它们指向词法环境,本质是从词法环境中获取数据。因此,无论是词法环境组件还是变量环境组件,都有一个环境记录器和一个外部对象,其中环境记录器记录变量,outer指向父作用域,具体请参考ECMAScript6标准第8节,回头看上面的例子:bar()函数中的console.log(a),在这个环境记录器中找不到,所以我参考outer在window中找到变量a,赋值后弹出,执行foo()后也会弹出,baz()后执行后会弹出一个ndleave在讲全局执行上下文在栈底之前的词法环境时,我们是这样定义的:outer指的是词法环境的父词法环境(作用域),但是这里有个疑问。既然outer指的是词法环境Parent作用域,那么作用域链从何而来呢?以上面的demo为例,bar的执行上下文伪代码:BarExecutionContext={ThisBinding=,LexicalEnvironment={EnvironmentRecord:{...},outer:},VariableEnvironment={EnvironmentRecord:{。..},外部: