JavaScript引擎执行流程深度解析:从全局初始化到作用域链
本文深入解析JavaScript引擎从代码定义到执行的全流程。通过全局初始化、函数A及函数B三个阶段的详细推演,阐述执行环境栈、变量对象、活动对象及作用域链的工作机制,揭示标识符解析与变量查找的底层逻辑。
JavaScript引擎执行流程概述
JavaScript从代码定义到实际执行的过程中,JS引擎在底层执行了大量初始化操作。为了深入理解JS引擎的工作机制,我们需要先掌握几个核心概念:执行环境栈、全局对象、执行环境、变量对象、活动对象、作用域以及作用域链。这些概念构成了JS引擎运行的核心组件。本文旨在通过一个具体的DEMO示例,系统性地分析JS引擎从定义到执行的完整细节,并阐释上述概念在其中的具体作用,而非孤立地讲解单一概念。
var x = 1; //定义一个全局变量 x
function A(y){
var x = 2; //定义一个局部变量 x
function B(z){ //定义一个内部函数 B
console.log(x+y+z);
}
return B; //返回函数B的引用
}
var C = A(1); //执行A,返回B
C(1); //执行函数B,输出 4
接下来,我们将分三个阶段深入分析JS引擎的工作机制:全局初始化、执行函数A以及执行函数B。
全局初始化阶段分析
1. 全局初始化阶段
当JS引擎开始执行一段代码时,必须完成以下三项初始化任务:
首先,JS引擎会创建一个全局对象(Global Object)。该对象在整个应用程序生命周期中仅存在一份,其属性可在任何位置访问。在创建全局对象时,引擎会将Math、String、Date、document等常用JS内置对象作为其属性挂载。由于全局对象无法直接通过名称访问,引擎会额外创建一个名为window的属性,并让其指向全局对象自身,从而允许通过window访问该全局对象。伪代码模拟的全局对象大致结构如下:
//创建一个全局对象
var globalObject = {
Math:{},
String:{},
Date:{},
document:{}, //DOM操作
...
window:this //让window属性指向了自身
}
随后,JS引擎构建执行环境栈(Execution Context Stack),同时创建一个全局执行环境(Execution Context,简称EC),并将该全局EC压入执行环境栈中。执行环境栈的作用是确保程序按正确顺序执行。在JavaScript中,每个函数拥有独立的执行环境;当执行某个函数时,其执行环境会被推入栈顶并获得执行权。函数执行完毕后,其执行环境从栈顶弹出,执行权交还给之前的执行环境。伪代码模拟的执行环境栈与EC关系如下:
var ECStack = []; //定义一个执行环境栈,类似于数组
var EC = {}; //创建一个执行空间,
//ECMA-262规范并没有对EC的数据结构做明确的定义,你可以理解为在内存中分配的一块空间
ECStack.push(EC); //进入函数,压入执行环境
ECStack.pop(EC); //函数返回后,删除执行环境
最后,JS引擎创建一个与EC关联的全局变量对象(Variable Object,简称VO),并将VO指向全局对象。VO不仅包含全局对象的原有属性,还涵盖全局定义的变量x和函数A。在定义函数A时,引擎为其添加内部属性[[scope]],并将scope指向VO。每个函数在定义时都会创建关联的scope属性,该属性始终指向函数定义时所处的环境。此时的ECStack结构如下:
ECStack = [ //执行环境栈
EC(G) = { //全局执行环境
VO(G):{ //定义全局变量对象
... //包含全局对象原有的属性
x = 1; //定义变量x
A = function(){...}; //定义函数A
A[[scope]] = this; //定义A的scope,并赋值为VO本身
}
}
];
函数A执行机制
2. 执行函数A阶段
当执行进入A(1)时,JS引擎需完成以下工作:
首先,JS引擎创建函数A的执行环境EC,将其推入执行环境栈顶并获得执行权。此时栈中包含两个执行环境:底部的全局执行环境和顶部的函数A执行环境。
接着,创建函数A的作用域链(Scope Chain)。在JavaScript中,每个执行环境拥有独立的作用域链,用于标识符解析。当执行环境被创建时,其作用域链初始化为当前运行函数的scope所包含的对象。
随后,JS引擎创建当前函数的活动对象(Activation Object,简称AO)。活动对象在此处扮演变量对象的角色,仅在函数内部使用此称呼(可理解为变量对象是总概念,活动对象是其分支)。AO包含函数的形参、arguments对象、this对象以及局部变量和内部函数的定义,随后AO被推入作用域链顶端。需要注意的是,在定义函数B时,JS引擎同样为B添加scope属性,并指向定义B时的环境。定义B的环境即为A的活动对象AO,且AO位于链表前端。由于链表具有首尾相连特性,函数B的scope指向了A的整个作用域链。此时的ECStack结构如下:
ECStack = [ //执行环境栈
EC(A) = { //A的执行环境
[scope]:VO(G), //VO是全局变量对象
AO(A) : { //创建函数A的活动对象
y:1,
x:2, //定义局部变量x
B:function(){...}, //定义函数B
B[[scope]] = this; //this指代AO本身,而AO位于scopeChain的顶端,因此B[[scope]]指向整个作用域链
arguments:[],//平时我们在函数中访问的arguments就是AO中的arguments
this:window //函数中的this指向调用者window对象
},
scopeChain:<AO(A),A[[scope]]> //链表初始化为A[[scope]],然后再把AO加入该作用域链的顶端,此时A的作用域链:AO(A)->VO(G)
},
EC(G) = { //全局执行环境
VO(G):{ //创建全局变量对象
... //包含全局对象原有的属性
x = 1; //定义变量x
A = function(){...}; //定义函数A
A[[scope]] = this; //定义A的scope,A[[scope]] == VO(G)
}
}
];
函数B执行与变量解析
3. 执行函数B阶段
函数A执行完毕后,返回B的引用并赋值给变量C。执行C(1)等同于执行B(1),JS引擎需完成以下工作:
首先,与前述步骤类似,创建函数B的执行环境EC,并将其推入执行环境栈顶获得执行权。此时栈中包含两个执行环境:底部的全局执行环境和顶部的函数B执行环境(注意:函数A返回后,其执行环境已从栈中移除,仅保留全局执行环境)。接着,创建函数B的作用域链,初始化为函数B的scope所包含的对象,即包含A的作用域链。最后,创建函数B的活动对象AO,并将B的形参z、arguments对象和this对象作为AO的属性。此时ECStack结构如下:
ECStack = [ //执行环境栈
EC(B) = { //创建B的执行环境,并处于作用域链的顶端
[scope]:AO(A), //指向函数A的作用域链,AO(A)->VO(G)
var AO(B) = { //创建函数B的活动对象
z:1,
arguments:[],
this:window
}
scopeChain:<AO(B),B[[scope]]> //链表初始化为B[[scope]],再将AO(B)加入链表表头,此时B的作用域链:AO(B)->AO(A)-VO(G)
},
EC(A), //A的执行环境已经从栈顶被删除,
EC(G) = { //全局执行环境
VO:{ //定义全局变量对象
... //包含全局对象原有的属性
x = 1; //定义变量x
A = function(){...}; //定义函数A
A[[scope]] = this; //定义A的scope,A[[scope]] == VO(G)
}
}
];
当函数B执行“x+y+z”时,需对x、y、z三个标识符进行解析,解析过程遵循变量查找规则:先查找自身活动对象中是否存在该属性,若存在则停止查找并返回;若不存在,则沿作用域链从顶端依次向下查找,直至找到。若整个作用域链均未找到该变量,则返回“undefined”。从上述分析可知,函数B的作用域链结构为:
AO(B)->AO(A)->VO(G)
因此,变量x在AO(A)中被找到,无需查找VO(G)中的x;变量y同样在AO(A)中找到;变量z则在自身的AO(B)中找到。最终执行结果为:2+1+1=4。
以上为本文全部内容,希望对读者学习有所帮助,也欢迎大家支持本站。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
