本文深入解析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
该Demo涉及闭包,最终执行结果为4。接下来,我们将分三个阶段深入剖析JS引擎的工作机制:全局初始化、执行函数A、以及执行函数B。
全局初始化阶段
一、全局初始化 当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时,引擎还会为A添加一个内部属性scope,指向VO。每个函数在定义时都会创建关联的scope属性,该属性始终指向定义时的环境。此时的ECStack结构如下:
ECStack = [ //执行环境栈
EC(G) = { //全局执行环境
VO(G):{ //定义全局变量对象
... //包含全局对象原有的属性
x = 1; //定义变量x
A = function(){...}; //定义函数A
A[[scope]] = this; //定义A的scope,并赋值为VO本身
}
}
];
函数A的执行过程
二、执行函数A 当执行进入A(1)时,JS引擎需完成以下工作: 首先,JS引擎创建函数A的执行环境EC,将其推入执行环境栈顶并获取执行权。此时栈中包含两个执行环境:底部的全局执行环境和顶部的函数A执行环境。接着,JS引擎创建函数A的作用域链(Scope Chain)。在JavaScript中,每个执行环境拥有独立的作用域链用于标识符解析;执行环境创建时,其作用域链初始化为当前运行函数的scope所包含的对象。 随后,JS引擎创建当前函数的活动对象(Activation Object, AO)。AO在此处扮演变量对象的角色,仅在函数语境下称为活动对象(可视为变量对象的一个分支)。AO包含函数的形参、arguments对象、this对象、局部变量及内部函数定义,随后AO被推入作用域链顶端。需注意,在定义函数B时,JS引擎同样为B添加scope属性,指向定义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的执行与闭包解析
三、执行函数B 函数A执行完毕后,返回B的引用并赋值给变量C。执行C(1)等同于执行B(1),JS引擎需完成以下工作: 首先,与前述步骤类似,JS引擎创建函数B的执行环境EC,推入栈顶并获取执行权。此时栈中包含全局执行环境和函数B的执行环境(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引擎工作机制后,不应仅停留在概念层面,而应将其作为基础工具,用于优化和改善实际工作中的代码,提高执行效率并产生实际价值。以变量查找机制为例,若代码嵌套较深,每次引用全局变量(如位于作用域链最底端的window和document对象),JS引擎都需遍历整个作用域链,这可能导致性能问题。因此,可围绕此问题开展多项性能优化工作。当然,优化方向不止于此,此处不再赘述,本文旨在抛砖引玉。
by @一像素 2015
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。