JavaScript 执行上下文

2025年3月17日 | 阅读13分钟

对于 JavaScript 开发者或想要深入了解 JavaScript 工作原理的人来说,这个主题非常重要。

在本节中,我们将学习和理解 JavaScript 的执行上下文,包括它是什么、类型、执行栈,执行上下文是如何创建的以及关于执行阶段的所有内容。我们将逐点讨论。让我们先从介绍部分开始。

什么是执行上下文?

执行上下文是描述代码内部工作原理的概念。在 JavaScript 中,使 JavaScript 代码能够执行的环境称为 JavaScript 执行上下文。正是执行上下文决定了代码的哪个部分可以访问代码中使用的函数、变量和对象。在执行上下文中,特定的代码会被逐行解析,然后变量和函数会被存储在内存中。执行上下文类似于一个容器,用于存储变量,然后代码会被求值并执行。因此,执行上下文为特定代码的执行提供了环境。

执行上下文的类型

JavaScript 中执行上下文的类型有:

  • 全局执行上下文/GEC
  • 函数执行上下文/FEC
  • Eval 执行上下文

开始逐一讨论

全局执行上下文

GEC / 全局执行上下文也称为基础/默认执行。任何不位于任何函数中的 JavaScript 代码都将存在于全局执行上下文中。其名称“默认执行上下文”的原因在于,当文件首次在 Web 浏览器中加载时,代码就开始执行。GEC 执行以下两项任务:

  • 首先,它创建一个全局对象,对于 Node.js 来说是全局对象,对于浏览器来说是 Window 对象。
  • 其次,将 Window 对象引用给 'this' 关键字。
  • 创建一个内存堆(memory heap)以存储变量和函数引用。
  • 然后,它将所有函数声明存储在内存堆区域,并将 GEC 中的变量的初始值设置为 'undefined'。

根据以上介绍,应该理解的是,每个代码中只有一个全局执行上下文,因为 JS 引擎是单线程的,因此,只有一个全局环境可以用于执行 JavaScript 代码。

函数执行上下文

FEC 或函数执行代码是 JavaScript 引擎在找到任何函数调用时创建的一种上下文。每个函数都有自己的执行上下文,因此与 GEC 不同,FEC 可以不止一个。此外,FEC 可以访问 GEC 的全部代码,但 GEC 无法访问 FEC 的全部代码。在 GEC 代码执行期间,会启动一个函数调用,当 JS 引擎找到它时,会为该特定函数创建一个新的 FEC。

Eval 函数执行上下文

在 eval 函数中执行的任何 JS 代码都会创建并保留自己的执行上下文。但是,eval 函数不被 JavaScript 开发者使用,但它是执行上下文的一部分。

执行栈

执行栈也称为 调用栈

栈是一种数据结构,它以 LIFO(后进先出)的形式存储值。同样,执行栈是一个跟踪脚本生命周期中创建的所有执行上下文的栈。JavaScript 开发者必须知道 JavaScript 是单线程工作的,它一次只能在 Web 浏览器中执行一项任务。因此,对于其他操作、函数和事件,会创建一个栈,称为 执行栈。执行栈的底部是 GEC,它是默认存在的。因此,当开始 JS 代码执行时(即,在 GEC 执行期间),如果代码中存在任何函数,并且 JS 引擎搜索它,它会立即为该函数创建一个函数执行上下文 (FEC) 并将其推送到执行上下文栈的顶部。位于执行上下文栈顶部的特定执行上下文将始终由 JS 引擎首先执行。一旦所有代码的执行完成,JS 引擎就会弹出函数的执行上下文,然后继续下一个,依此类推。通常,当脚本在浏览器中加载时,第一个元素将是全局执行上下文。但是,当检测到函数执行时,就会创建执行上下文并将其虚拟放置在 GEC 的顶部。此过程将一直持续到整个代码执行完成。

为了理解执行栈的工作原理,让我们看下面的示例代码

这是一个用于理解其工作原理的示例代码。

说明

  • 首先,所有代码都加载到浏览器中。
  • 之后,JS 引擎将 GEC 推送到/插入到执行栈的顶部。
    JavaScript Execution Context
  • 一旦 JS 引擎遇到第一个函数调用,它就会为其设置一个新的 FEC 并将其添加到当前执行栈的顶部。
    JavaScript Execution Context
  • 然后,我们可以看到这是在第一个函数内调用第二个函数。因此,JS 引擎为第二个函数设置一个新的 FEC 并将其插入到栈顶。
    JavaScript Execution Context
  • 当第二个函数完成时,执行函数会从栈中弹出,并将控制权移至栈中存在的下一个执行上下文,即第一个函数执行上下文。
    JavaScript Execution Context
  • 当第一个函数完全执行完毕后,第一个函数的执行栈会从栈中弹出。因此,控制权会返回到代码的 GEC。
    JavaScript Execution Context
  • 最后,当整个代码的执行完成时,JS 引擎会从当前栈中移除 GEC。

执行栈的执行就这样进行。

创建执行上下文

首先创建一个执行上下文,然后对其进行管理。执行上下文的创建有两种方式:

创建阶段

创建阶段是指 JS 引擎调用函数但尚未开始执行的阶段。在此阶段,JS 引擎开始其编译阶段,扫描特定函数的代码进行编译,但不执行代码。执行上下文的创建由 JavaScript 引擎负责,它通过执行以下描述的任务来创建:

任务 1:创建激活对象/变量对象: JavaScript 的一个特殊对象,类似于一个容器,其中包含函数参数、变量以及内部函数声明的所有信息。它没有 dunder proto 属性。

任务 2:创建作用域链: 完成任务 1 后,JS 引擎初始化作用域链。作用域链是一个列表,包含当前函数存在的范围内的所有变量对象。作用域链还包含 GEC 的变量对象,并携带当前函数的变量对象。

  • 确定 'this' 值: 创建作用域链后,JS 引擎会初始化 'this' 的值。

让我们通过下面的例子来理解激活对象的创建

示例代码 1

现在,在调用 test() 之后,执行其代码之前,JS 引擎为示例代码 1 创建了一个 executionContextObj。如下面的代码所示:

激活对象包含参数对象,该对象进一步包含关于函数参数的详细信息。它具有当前函数中声明的每个函数和变量的属性名称。在我们的例子中,示例代码 1 的激活对象将是:

说明

  • 如您在上面的代码中看到的,JS 引擎已创建了参数对象。还有一个 length 属性,其中包含函数中参数的总数。它只有属性名,没有值。
  • 在此之后,对于函数中值为 'undefined' 的每个变量,JS 引擎会在激活对象或变量对象上设置一个属性。这些参数也是函数中的变量,因此也是参数对象的属性。
  • 如果变量已经作为参数对象属性存在,那么 JS 引擎将继续执行,而无需采取进一步行动。
  • 当 JS 引擎在当前函数中找到函数定义时,它会使用函数名创建一个新属性。如上所述,函数定义存储在堆内存中。函数名属性指向其在堆内存中的定义。
  • 因此,我们可以看到上面的代码中 w 是一个变量。因此,它将获得 'undefined' 的值。但是,当找到同名函数时,会发生覆盖,其值将指向存储在堆内存中的函数 w 的定义。之后,JS 引擎会设置作用域链并确定 'this' 的值。

因此,创建阶段就是这样工作的。

执行阶段

执行阶段是创建阶段完成后的下一个阶段。执行阶段是 JS 引擎再次扫描代码中的函数,即再次更新变量对象中的变量值并运行代码的阶段。让我们看看我们上面讨论的示例的执行阶段或完整代码:

示例代码 2

首先,上面的代码在浏览器中加载。之后,JS 引擎开始其编译阶段以创建执行对象。在编译阶段,JS 引擎仅处理声明,而不关心值。

现在,在执行阶段,将按照每行描述的顺序执行以下步骤:

  1. 变量 x 被赋值为 10,这使得 JS 引擎不再将其视为函数声明或变量声明,而是继续进行,即转到第三行。在第三行,它不做任何事情,因为它不是任何声明。
  2. 接下来,JS 引擎会在 GEC 对象中设置一个名为 'z' 的属性(因为 z 是一个变量名,并且位于全局声明作用域),并将其初始值设置为 'undefined'。
  3. 移动到代码的第五行,JS 引擎遇到了一个函数声明。JS 引擎会将函数定义存储在堆内存中,然后设置一个指向该特定堆内存位置的属性。函数中存储了什么并不重要,它只是指向其位置。
  4. 正如我们在最后一行中看到的,它不是代码的任何声明。因此,JS 引擎不会执行任何操作。

因此,创建阶段和执行阶段的工作就是这样进行的。

创建阶段后的 GEC 对象

在上面的解释中,我们看到了如何为示例代码 2 通过执行和创建阶段创建执行栈。然而,我们应该更深入地理解上述代码的 GEC 和 FEC 的工作原理。考虑示例代码 2 的以下 GEC 对象代码:

如您在上面看到的,没有剩余代码了,JS 引擎会进入执行阶段,再次扫描函数。JS 引擎会按照下面每行描述的方式更新变量值然后执行代码:

  • 首先,JS 引擎发现变量对象中没有名为 x 的属性,因此它将此属性添加到 GEC 中并将其值初始化为 10。
  • 接下来,JS 引擎发现变量对象中存在名为 y 的属性,因此将其值从 20 更新。
  • 最后,JS 引擎不执行任何操作,因为这是一个函数声明。

现在,让我们看看执行阶段后的 GEC 对象。

执行阶段后的 GEC 对象

在我们的示例代码 2 中:

  • 现在,当 z() 再次被调用时,JS 引擎再次进入编译阶段。因此,它扫描函数以创建其执行上下文对象。
  • 函数 z() 的参数是 'val',因此 JS 引擎会将 'val' 添加到 z() 执行上下文对象的参数对象中。然后它创建一个名为 'val' 的属性。
  • 接下来,它检查并发现 p 是否是函数激活对象中的一个属性。因此,它发现不存在这样的属性,所以它将 p 添加为属性,然后将其值初始化为 'undefined'。
  • 接下来,JS 引擎的职责是查看 q 是否是函数激活对象中的一个属性。因此,它发现不存在这样的属性,所以它将 q 添加为属性,然后将其值初始化为 'undefined'。
  • 然后,JS 引擎继续下一行,因为 x = 30 不是声明。
  • 然后,JS 引擎遇到了一个 test() 函数声明,并为此,它将函数定义存储在堆内存区域。然后,它设置一个名为 'test' 的属性,该属性指向存储函数定义的位置。JS 引擎不关心其中存储的值。

编译阶段后的 z 执行对象

下面是编译阶段完成后 FEC 对象的代码:

示例代码 2 的编译阶段后的代码如下所示:

  • 这是 test() 函数调用,而不是声明,因此 JS 引擎不会执行任何操作。
  • 现在,JS 引擎将继续执行阶段,通过扫描 z() 函数来执行它。
  • 在执行阶段,变量 p 和 q 的值分别为 5 和 10。
  • 接下来,JS 引擎发现 x 不是声明,也不是 z 执行上下文对象上的任何属性,因此它通过作用域链移动到代码的 GEC,并在 GEC 中检查是否存在名为 x 的属性。如果未找到,JS 引擎将创建一个名为 x 的新属性并初始化它。在示例代码 2 中,JS 引擎发现 GEC 对象上已存在名为 x 的属性,因此它将其值从 10 更新为 30。应该注意的是,JS 引擎仅在这种情况下继续到 GEC,即当它在执行阶段找到一个不在当前执行上下文对象上的变量时。
  • 之后,JS 引擎设置一个 test 属性,然后指向其堆内存位置。

执行阶段后的 z 的执行上下文对象

在示例代码 2 中:

  • 在示例代码 2 中,JS 引擎再次进入编译阶段,为 'test' 创建执行上下文对象。
  • test 执行上下文对象通过作用域链可以访问 z 上定义的每个函数、变量,并且也可以访问全局作用域。
  • 同样,z 可以访问全局作用域中的所有变量和对象。但是,它无法访问 test 的变量和对象。
  • 代码的 GEC 无法访问 z 或 test 的变量或对象。

因此,执行上下文就是这样创建和定义的。

全局执行上下文与函数执行上下文

两者之间存在以下区别:

全局执行上下文函数执行上下文
它创建一个全局作用域。它创建一个参数对象。
它创建一个名为 'this' 的对象。它默认指向 Window 对象。
它为全局定义的函数和变量设置内存空间。它为仅在函数内定义的函数和变量设置内存空间。
GEC 在将任何函数声明设置到内存时,为变量声明分配默认值 'undefined'。FEC 在将任何函数声明设置到内存时,为变量声明分配默认值 'undefined'。此外,它还会创建自己的执行栈。