本文深入解析JavaScript执行机制,详解全局对象GO、全局执行上下文GEC及函数执行上下文FEC的创建过程。通过V8引擎解析代码、变量提升原理、作用域链查找机制及ECMAScript规范演进,结合代码示例剖析变量在内存中的生命周期与访问规则。
前言
在深入探讨标题之前,我们先通过一段代码来观察其运行过程。
var num1=1
var num1=2
var result=num1+num2
console.log(result)相信大多数开发者都能预判出打印结果为3,这看似简单,但我们需要深入思考代码执行时的内部操作。为此,本文将引入两个核心概念:全局对象的初始化以及执行上下文的构建。
初始化全局对象(GO)
当JavaScript引擎开始解析代码时,会在堆内存中创建一个名为全局对象(Global Object,简称GO)的对象。
- 该对象具有全局可见性,所有作用域均可访问它。
- GO中预置了如Date、Array、String、Number、setTimeout、setInterval等内置构造函数和方法。
- 此外,GO还包含一个指向自身的window属性。

执行上下文
在代码运行阶段,JavaScript引擎(以V8引擎为例)会创建一个执行上下文栈(Execution Context Stack,简称ECS),它充当了代码调用栈的角色。
执行上下文主要分为两类:
- 全局执行上下文(Global Execution Context,简称GEC)。
- 函数执行上下文(Functional Execution Context,简称FEC)。
鉴于开篇示例为全局代码,我们首先分析全局执行上下文的运作机制。
全局执行上下文
当全局代码开始执行时,会创建全局执行上下文(GEC),并将其压入ECS中执行。
GEC的构建包含两个主要阶段:
- 第一阶段:在代码正式执行前,即V8引擎将源代码解析为抽象语法树(AST)的过程中,全局声明的变量会被放入GO中,但此时不进行赋值操作。这一过程发生在解析阶段,通常被称为“变量提升”。
- 第二阶段:执行代码,为GO中的变量赋予实际值,或调用其他函数。具体流程可参考相关图示,图中“执行前”对应上述第一阶段,“开始执行代码”对应第二阶段。

JavaScript遇到函数代码如何执行?
当JavaScript引擎遇到函数调用时,会创建一个函数执行上下文(FEC),并将其压入执行上下文栈ECS中。
FEC包含三个关键部分:
- 第一部分:变量对象(Variable Object,简称VO),在函数执行环境中也被称为活动对象(Activation Object,简称AO)。
- 第二部分:作用域链(Scope Chain),由当前函数的VO及其父级VO组成,变量查找时逐层向上搜索。
- 第三部分:this的绑定(此处暂不展开,后续将单独讨论)。
作用域在编译解析阶段即已确定,而非在调用时动态决定。让我们通过一段代码来观察函数的执行过程,并思考其运行结果。
var message = "Global"
function foo() {
console.log(message)
}
function bar() {
var message = "Bar"
foo()
}
bar()我们可以通过图示来分析这一过程。
首先初始化全局对象GO,其中包含Array、date、setTime等内置对象,以及用户自定义的message对象初始值为undefined,foo和bar函数对象。


在代码执行前,会创建执行上下文栈ESC。由于开始执行全局代码,因此创建全局执行上下文栈GEC,并将GEC压入栈底。GEC包含两部分:
第一部分:代码执行前的VO对象(这里的VO指向的是GO)。
第二部分:开始执行代码,执行第一行时var message="Global",GO对象里的message就被改为Global。

执行完第一行后,开始执行第9行代码bar()。这是函数调用,JavaScript引擎会创建一个函数执行上下文FEC并压入栈中。FEC包含三部分:
第一部分:VO对象,这里指向bar函数自己的AO对象(包括形参和函数中定义的变量)。
第二部分:作用域链scope chain,包含自己的VO对象+parent VO对象。
第三部分:this绑定,这里指向window(后续将专题讲解)。
开始执行bar函数内部代码,var message="Bar",这会将bar的AO对象中的message变量值从从undefined改为"Bar"更新。接下来执行foo()函数,注意:此时bar函数尚未弹出栈,因为foo函数仍在执行中。

访问变量时,会沿着作用域链逐层向上查找,若最终未找到则报错。
执行到bar函数的最后一行代码foo()时,再次发生函数调用,创建foo的函数执行上下文,同样包含上述三部分:VO对象(指向foo的AO对象)、作用域链和this绑定。
开始执行foo函数代码console.log(message),当打印message时,会沿着作用域链一层层找,foo的作用域链是自己的AO+父级的VO(也就是GO对象),由于foo的作用域链中自己的AO对象为空,因此向上找到GO中的message变量,最终打印“Global”。

函数执行完毕后,其执行上下文将从栈中弹出。foo的FEC先弹出,随后bar的FEC弹出,它们各自关联的AO对象最终也被释放。
环境变量和记录
上述提到的VO基于早期的ECMAScript规范,官方文档指出:

借助coderwhy的翻译:每个执行上下文都关联一个变量对象(Vriable Object),源代码中的变量和函数声明作为属性加入变量对象。对于函数,参数也会加入VO。但ECMAScript 5之后的版本对术语进行了修改。

每个执行上下文关联一个变量环境(variable Environment)),在执行代码中,变量和函数的声明作为环境记录(Environment Record)加入变量环境。
对于函数代码,形参也作为环境记录加入变量环境。
总结:
- 访问变量时,沿作用域链逐层向上查找,若最终未找到,会报错:未定义。
- 作用域及其父级作用域在代码编译阶段确定,与调用位置无关。
汇总一些名词解释,我们来解释一下:
| 名词 | 解释 |
|---|---|
| ECS | 执行上下文栈 (Execution Context Stack),也可称为调用栈 |
| GEC | 全局执行上下文(Global Execution Context),在执行全局代码前创建 |
| FEC | 函数执行上下文(Functional Execution Context),在执行函数前创建 |
| VO | Variable Object,早期ECMA规范中的变量环境,对应Object |
| VE | Variable Environment,最新ECMA规范中的变量环境,对应环境记录 |
| GO | 全局对象(Global Object),解析全局代码时创建,GEC中关联的VO就是GO |
| AO | 活动对象(Activation Object),VO被激活就变成了AO,VO和AO本质上是一个东西,它们其实都是同一个对象,只是处于执行环境的不同生命周期 |
到此这篇关于深入学习JavaScript执行上下文的文章就介绍到这了,更多相关JS执行上下文内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!