JavaScript 引擎运行机制:从全局初始化到作用域链解析

作者:函数茶馆时间:2026-10-30 17:42:25浏览:304

本文详细解析 JavaScript 引擎从代码定义到执行的全过程。通过闭包示例,逐步剖析全局初始化、函数 A 执行及函数 B 执行三个阶段。重点讲解全局对象、执行环境栈(ECStack)、变量对象(VO)与活动对象(AO)的创建与交互,以及作用域链在标识符解析中的核心机制,帮助开发者深入理解 JS 底层工作原理。

JavaScript 引擎工作机制概述

JavaScript 引擎工作机制

从代码定义到最终执行,JavaScript 引擎在底层执行了大量初始化操作。为了深入理解这一机制,我们需要先掌握几个核心概念:执行环境栈、全局对象、执行环境、变量对象、活动对象、作用域以及作用域链。这些概念构成了 JavaScript 引擎运行的基础组件。本文并非孤立地解释每个术语,而是通过一个具体的演示案例,系统梳理 JavaScript 引擎从定义到执行的完整细节,并阐明上述概念在其中的具体作用。



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。接下来,我们将分三个阶段深入分析 JavaScript 引擎的工作流程:全局初始化、函数 A 的执行以及函数 B 的执行。

全局初始化阶段分析

一、全局初始化阶段

当 JavaScript 引擎开始执行一段代码时,必须完成以下三项初始化任务:

首先,引擎会创建一个全局对象(Global Object)。该对象在整个应用程序生命周期中仅存在一份实例,其属性可在任何位置访问。在创建全局对象时,引擎会将 Math、String、Date、document 等常用 JavaScript 内置对象作为其属性挂载。由于全局对象无法直接通过名称访问,引擎还提供了一个名为 window 的属性指向其自身,从而允许通过 window 对象访问全局上下文。以下伪代码展示了全局对象的大致结构:


//创建一个全局对象
var globalObject = { 
  Math:{},
  String:{},
  Date:{},
  document:{}, //DOM操作
  ...
  window:this //让window属性指向了自身
}

其次,JavaScript 引擎需要构建执行环境栈(Execution Context Stack)。同时,引擎会创建一个全局执行环境(Execution Context,简称 EC),并将其压入执行环境栈中。执行环境栈的主要作用是确保程序代码按照正确的顺序执行。在 JavaScript 中,每个函数都拥有独立的执行环境;当调用一个函数时,其对应的执行环境会被推入栈顶并获得执行权。函数执行完毕后,该执行环境从栈顶弹出,执行权交还给之前的执行环境。以下伪代码模拟了执行环境栈与执行环境(EC)之间的关系:


var ECStack = []; //定义一个执行环境栈,类似于数组

var EC = {};  //创建一个执行空间,
//ECMA-262规范并没有对EC的数据结构做明确的定义,你可以理解为在内存中分配的一块空间

ECStack.push(EC); //进入函数,压入执行环境
ECStack.pop(EC); //函数返回后,删除执行环境

最后,引擎创建一个与全局执行环境关联的全局变量对象(Variable Object,简称 VO),并将 VO 指向全局对象。VO 不仅包含全局对象原有的属性,还包含全局范围内定义的变量 x 和函数 A。此外,在定义函数 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 的执行过程

二、执行函数 A

当代码执行进入函数 A(1) 时,JavaScript 引擎需执行以下操作:

首先,引擎为函数 A 创建执行环境(EC),并将其推入执行环境栈的顶部以获取执行权。此时,执行环境栈中包含两个执行环境:位于栈底的全局执行环境和位于栈顶的函数 A 执行环境。接着,引擎构建函数 A 的作用域链(Scope Chain)。在 JavaScript 中,每个执行环境都拥有独立的作用域链,用于标识符解析。当执行环境被创建时,其作用域链初始化为当前运行函数的 scope 所指向的对象。

随后,引擎创建函数 A 的活动对象(Activation Object,简称 AO)。在此语境下,活动对象扮演变量对象的角色,区别仅在于命名(可理解为变量对象是总称,而活动对象是其分支)。AO 包含函数的形参、arguments 对象、this 对象、局部变量以及内部函数的定义,随后 AO 被推入作用域链的顶端。需要注意的是,在定义函数 B 时,JavaScript 引擎同样会为 B 添加 scope 属性,并将其指向定义函数 B 时的环境。由于函数 B 定义在函数 A 内部,其 scope 指向函数 A 的活动对象 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),此时 JavaScript 引擎需完成以下工作:

首先,引擎创建函数 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 会在函数 A 的活动对象(AO(A))中被找到,无需继续查找全局变量对象(VO(G))中的 x;变量 y 同样在函数 A 的活动对象(AO(A))中找到;变量 z 则在函数 B 自身的活动对象(AO(B))中找到。最终执行结果为:2 + 1 + 1 = 4。

简要总结

理解 JavaScript 引擎的工作机制不应仅停留在概念层面,而应将其作为基础工具,用于优化和改善实际工作中的代码,提高执行效率,从而产生实际价值。以变量查找机制为例,如果代码嵌套层级较深,每次引用全局变量时,JavaScript 引擎都需要遍历整个作用域链。例如,位于作用域链最底端的 window 和 document 对象就存在此类性能问题。因此,围绕这一问题可以实施多种性能优化策略。当然,还有其他方面的优化手段,此处不再赘述,本文旨在抛砖引玉。

感谢阅读,希望能对大家有所帮助,感谢各位对本站的支持!

作者最新文章