【译】JavaScript 执行上下文1 — 从编译到执行

121 阅读5分钟

对于许多人来说,JavaScript 是一个谜。它具有独特的特点。

了解后,您可能听说过以下术语:

  • 变量提升 Hoisting
  • 作用域和作用域链 Scope and scope chain
  • 闭包 Closure
  • this

它们都具有 JavaScript 中独有的“奇怪”行为。

揭开概念神秘面纱的关键是执行上下文 ( Execution Context )。我希望这些文章能够提供一个不同的视角来理解 JavaScript,而不是照搬 MDN 上准确但晦涩的定义。

别误会我的意思。概念的定义很有用。我从 MDN 上阅读了描述。然而,我相信理解“如何”比记住“什么”更重要。

在本系列结束时,您可能会得出对所有这些术语的定义,这对您来说更有意义。 因此,这些信息就成为你的知识。

两个步骤:编译 compile 和执行 execution

当一段JavaScript代码运行时,我们讨论的是两个步骤:编译和执行。 看起来很简单。然而,所有的谜团都隐藏在这两个步骤之中。

JavaScript 被编译时会发生什么?创建执行上下文。当执行上下文准备就绪时,执行步骤开始。所有可执行的 JavaScript 代码都是逐行运行的。

image.png

执行上下文由几部分组成。为了理解它,我们关注四个组成部分:

  • Variable environment 变量环境
  • Lexical environment 词法环境
  • Outer
  • this

image.png

在这篇文章中,Variable environment 是主角。我们暂时忽略其他组件。

image.png

当执行步骤时,浏览器会逐行运行 JavaScript。同时,每当一行完成运行时,执行上下文就会更新。

image.png

变量和变量环境 Variable Env

让我们从一个例子开始。

image.png

当这段 JavaScript 运行时,第一步是编译。

在此步骤中,将创建执行上下文。

image.png

同时,变量 apple 被声明为未定义值并存储在 Variable Env 中。

image.png

编译步骤结束,执行步骤开始。

当执行第一行时,apple 变量被赋值为 10。同时 Variable Env 也被更新。

image.png

然后执行第二行。控制台开始在其 Variable Env 中查找 apple 变量,并找到了 apple。控制台打印 10。

整个过程结束。整个执行上下文被删除。

从这些示例中,我们可以得出以下关键结论:

  • 变量赋值实际上分为两个步骤。
  • 编译步骤负责变量声明。
  • 执行步骤执行变量的赋值和其余代码。

变量提升 Hoisting

image.png

这段代码与上一段代码略有不同。它在声明变量之前打印 apple 变量。 我们知道控制台会输出 undefined,但让我们从执行上下文的角度一步步看一下。

在编译步骤中,会跳过第 1 行,因为它与变量声明无关。然后在变量环境中创建第2行中的apple变量。编译结束。

image.png

在第一行中,控制台开始在其变量环境中查找 apple 变量。此时,apple 的值未定义。console.log 输出 undefined。

image.png

然后执行第二行,将 apple 变量更新为值 10。整个过程结束。

image.png

我们称这个过程为变量提升 hoisting,因为变量 apple 感觉就像被提升到了顶部。下面的伪代码模拟了提升效果。

image.png

然而,从执行上下文的角度来看,没有提升。没有任何东西被提升。发生这种情况是因为变量是在编译步骤中声明的。从结果来看是被提升了。

函数的提升

函数略有不同,因为我们有两种声明函数的选项。

image.png

声明 showName 函数时会发生赋值。相比之下,showNumber 函数是一个没有赋值的声明。

有趣的是,我们将看到控制台打印“Hey, show a number”,后面跟着一条错误消息“showName is not a function”。

让我们回顾一下这两个步骤,看看发生了什么。

在编译阶段,showNumber 是一个声明,因此此时它实际上被存储。

image.png

当涉及到 showName 函数时,因为它是一个赋值,所以它被赋值为 undefined。

image.png

showName 功能块直到执行步骤才被分配。

image.png image.png image.png

如果您打印 showName 的值而不是在第 2 行中执行它,控制台将打印 undefined。undefined 不是函数,因此会显示错误消息“showName is not a function”。

值得一提的是,图中关于函数的内容并不完全准确。函数体保存在一个名为 HEAP 的地方,而不是变量环境中。

image.png

当调用函数时,浏览器在 HEAP 中查找函数体,而不是变量环境。

为了简单起见,我将把 HEAP 放在一边,并通过帖子将变量环境中的函数保留在图形中。

两个不寻常的案例来了解编译技巧

第一种情况是命名冲突。

我们知道,如果我们使用相同的变量名,后一个变量将覆盖第一个变量名。

你知道控制台会打印什么吗?

image.png

控制台会打印消息“我是一个声明”,尽管该变量是稍后声明的。第二个 showNumber 变量不会覆盖第一个变量。

当函数和变量具有相同名称时,变量声明在编译步骤中将被忽略。换句话说,功能具有优先级。

这是一个很容易被忽视的陷阱,所以我们应该避免使用相同的变量名。

让我们看另一个例子。

image.png

这里,if中的条件是0,即负值。 if 块中的代码永远不会运行。

如果运行代码,控制台会打印“未定义”而不是“apple 未定义”错误消息。

在这种情况下,apple 变量仍然在编译步骤中在变量环境中声明。

if 条件仅适用于执行步骤,不适用于编译步骤。

要点

  • JavaScript 的运行需要两个步骤:编译和执行。
  • 执行上下文是在编译步骤创建的,由变量环境和其他组件组成。
  • 变量在编译步骤中声明并在执行步骤中赋值。