JavaScript 执行上下文 - JS 代码如何在幕后运行

在了解什么是 JavaScript 执行上下文之前,我们需要知道如何以及在什么环境中运行 JavaScript 代码。

首先,我们可以在两种环境中运行 JavaScript:

  • 通过浏览器
  • 通过 Node.js
  • JavaScript代码在我们的计算机上是如何运行的?

    当我们在计算机上编写 JavaScript 代码并尝试运行它时,代码首先转到浏览器或 Node.js。

    但是,我们编写的 JavaScript 代码并不能被浏览器或 Node.js 直接理解。此时,两者都将代码发送给内置的 JavaScript 引擎。引擎有多种类型,例如:

  • Google Chrome 中的 V8 引擎,
  • Mozilla Firefox 中的 SpiderMonkey,
  • Node.js 中的 V8 引擎等
  • 接下来,JavaScript 引擎将 JavaScript 代码编译为机器代码。然后,该机器代码被发送到计算机,计算机执行它,然后我们就会看到显示的输出。

    作为程序员,我们需要很好地理解这个中间步骤,即 JavaScript 引擎如何将 JavaScript 代码编译成机器代码。

    那么,现在我们需要了解 JavaScript 引擎的工作原理。JavaScript 引擎通过两种方式将代码转换为机器代码。第一种是解释,第二种是编译。那么,什么是解释和编译?

    什么是口译?它是如何运作的?

    “解释”是逐行读取用高级语言编写的所有源代码并在读取后立即将每行转换为机器代码的过程。如果在读取某行代码时出现错误,则该过程立即停止,使程序员可以轻松识别错误。这使得调试变得简单。但是,由于此过程逐行读取代码,因此速度相对较慢。

    什么是编译?它是如何工作的?

    `编译` 是将用高级语言编写的所有源代码一次性转换为机器代码的过程。在这种情况下,即使代码中有错误,它仍会编译并仅在运行时显示错误。因此,程序员更难识别错误,使调试更具挑战性。但是,由于整个源代码一次性转换为机器代码,因此这个过程相对较快。那么现在,问题来了:JavaScript 是编译型语言还是解释型语言?

    JavaScript 是编译型语言还是解释型语言?

    最初,JavaScript 主要被认为是一种解释型语言。然而,由于这个过程相当缓慢,现代 JavaScript 引擎开始使用一种结合了“解释”和“编译”的新技术,称为“即时 (JIT) 编译”。此过程结合了解释和编译,将代码转换为机器代码。因此,与旧方法相比,它更快、更容易调试。

    要理解 JavaScript 的“即时 (JIT) 编译”的工作原理,我们需要了解 JavaScript 的“执行上下文”。现在让我们尝试理解 JavaScript 的执行上下文。

    JavaScript 执行上下文

    首先,看一下下面的代码示例。

    代码示例

    var a = 1;
    
    function one() {
      console.log(a);
    
      function two() {
        console.log(b);
    
        var b = 2;
    
        function three(c) {
          console.log(a + b + c);
        }
    
        three(4);
      }
    
      two();
    }
    
    one();

    输出

    1
    undefined
    7

    当我们运行代码时,我们尝试在 `two()` 函数内部声明 `b` 变量之前打印它,输出为 `undefined`。但是,没有发生任何错误。问题出现了:`b` 变量是如何获得值 `undefined` 的?答案在于 JavaScript 执行上下文。现在,我们将更详细地探讨 JavaScript 执行上下文。

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

  • 全局执行上下文
  • 函数执行上下文
  • 每个执行上下文都会经历两个阶段:“创建阶段”和“执行阶段”。

    全局执行上下文

    当我们运行 JavaScript 代码时,首先发生的事情是“全局执行上下文”。此上下文首先经历其“创建阶段”,其中会发生以下几件事:

    创建阶段

  • 创建了一个全局对象。
  • 创建一个此对象并赋予其全局对象的值。
  • 创建一个变量对象,其中声明了所有函数和变量。变量被赋予 undefined 作为其值,函数被赋予对其各自函数的引用。
  • 一旦创建阶段完成,`全局执行上下文`就会进入下一阶段:`执行阶段`,其中会发生更多步骤。

    执行阶段

  • 在创建阶段声明并用未定义初始化的变量现在被赋予各自的值。
  • 在创建阶段声明的函数(存储为引用)现在被调用和执行。
  • 函数执行上下文

    当调用全局执行上下文的“执行阶段”中引用的函数时,每个函数都会创建自己的“函数执行上下文”。与全局执行上下文一样,“函数执行上下文”也会经历“创建阶段”,其中会发生以下几个步骤:

    创建阶段

  • 为该函数创建一个参数对象。
  • 创建一个此对象并赋予其全局对象的值。
  • 创建一个变量对象,其中声明了所有函数和变量。变量被赋予 undefined 作为其值,函数被赋予对其各自函数的引用。
  • 一旦创建阶段完成,`函数执行上下文`就会转移到`执行阶段`,并发生更多步骤。

    执行阶段

  • 在创建阶段声明的变量,以未定义的方式初始化,现在被赋予了各自的值。
  • 现在调用并执行在创建阶段声明的函数。
  • 嵌套函数中的函数执行上下文

    当函数在其他函数中被调用时,会为每个函数创建一个新的“函数执行上下文”。然后,每个“函数执行上下文”都会经历“创建阶段”和“执行阶段”。对于每个在另一个函数中调用的函数,此过程都会继续,并且每个函数都会分别经历这些阶段。

    我们来看看下面的图表。

    Image description

    我们已经看到,`全局执行上下文`和`函数执行上下文`都会经历一些步骤。唯一的区别是,在`全局执行上下文`中,第一步是创建全局对象,而在`函数执行上下文`中,第一步是为函数创建`参数对象`。

    现在,问题出现了:当为全局上下文和每个函数创建这些“执行上下文”时,JavaScript 如何管理它们?

    使用执行堆栈管理执行上下文

    为了管理这些上下文,JavaScript 使用一种称为“执行堆栈”的数据结构。“执行堆栈”以类似堆栈的方式存储上下文:首先是“全局执行上下文”,然后是每个“函数执行上下文”。当所有执行上下文都存储在堆栈中时,JavaScript 会从堆栈顶部开始逐一处理它们。

    使用 let 和 const 确定作用域

    需要注意的是,当我们在全局或函数作用域内用 `let` 或 `const` 声明变量时,这些变量在 `创建阶段` 不会存储在 `变量对象` 中,也不会用 `undefined` 初始化,而是在 `执行阶段` 直接声明并赋值。

    考虑以下代码示例:

    代码示例

    function two() {
      console.log(b);
    
      const b = 2;
    }
    
    two();

    如果我们运行此代码,我们将遇到“ReferenceError”。这是因为我们试图在声明变量“b”之前打印其值,并且由于“b”使用“const”声明,因此其行为与常规变量不同。在“创建阶段”,使用“const”或“let”声明的变量不会存储在“变量对象”中,这就是为什么我们在为它们赋值之前尝试访问它们时会出错的原因。

    结论

    我希望本文对 JavaScript 的工作原理以及其“执行上下文”阶段发生的情况的解释能让您更清楚地理解。在下一课中,我们将探讨另一个 JavaScript 主题。

    您可以在 GitHub 和 Linkedin 上与我联系。