如果JavaScript是一种解释性语言,那么变量提升是如何工作的?

14

我理解的解释器是逐行执行程序,我们可以看到即时结果,不像编译语言将代码转换后再执行。

我的问题是,在Javascript中,解释器如何知道某个变量在程序中被声明,并将其记录为undefined

考虑以下程序:

function do_something() {
  console.log(bar); // undefined (but in my understanding about an interpreter, it should be throwing error like variable not declared)
  var bar = 111;
  console.log(bar); // 111
}

被默认为:

function do_something() {
  var bar;
  console.log(bar); // undefined
  bar = 111;
  console.log(bar); // 111
}
这怎么运作?

4
可能是为什么JavaScript会提升变量?的重复问题。 - yuriy636
你需要买一本 JavaScript 的书并阅读它。这被称为变量提升。 - user1228
1
你对“解释器”一词的理解是错误的。 - Pointy
1
这是一个合理的问题。我也在想同样的事情,哈哈。我想,如果代码首先被解释(逐行)以定义变量,然后再次解释以实际执行代码,会怎样呢?想想看,哈哈哈。 - Iván Sánchez
2个回答

22
这个“变量提升”概念如果只是表面理解的话会比较困惑。你需要深入了解语言本身的工作原理。JavaScript是ECMAScript的一种实现,是一种解释型语言,意味着你编写的所有代码都会被输入到另一个程序中,该程序反过来会“解释”代码并基于源代码的某些部分调用特定的函数。
例如,如果你写下以下代码:
function foo() {}

解释器一旦遇到您的函数声明,将调用其自己的函数FunctionDeclarationInstantiation来创建该函数。解释器不会将JavaScript编译成本机机器代码,而是在读取JavaScript代码的每个部分时按需执行自己的C、C++和机器代码。这并不一定意味着逐行执行,所有解释执行的意思是没有编译成机器代码。一个独立的程序执行机器码读取您的代码,并在运行时执行那些机器码。
关于变量声明提升或任何声明与此有关的问题,解释器首先会在不执行任何实际代码的情况下先读取您的所有代码一次。它会分析代码并将其分成块,称为词法环境。根据ECMAScript 2015语言规范:

8.1 词法环境

词法环境 是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构将 标识符 关联到特定变量和函数。词法环境由一个 Environment Record 和一个可能为 null 的对外部词法环境的引用组成。通常,词法环境与 ECMAScript 代码的某些特定语法结构(如 FunctionDeclarationBlockStatementTryStatementCatch 子句)相关联,并且每次评估此类代码时都会创建一个新的词法环境。

Environment Record 记录在其关联词法环境的作用域内创建的标识符绑定。它被称为词法环境的 EnvironmentRecord。

在执行任何代码之前,解释器遍历您的代码,并为每个词法结构(如函数声明、新块等)创建一个新的词法环境。在这些词法环境中,一个环境记录记录了该环境中声明的所有变量及其值以及有关该环境的其他信息。这就是允许 JavaScript 管理变量作用域、变量查找链、this 值等的原因。

每个词法环境都与一个代码领域相关联:

8.2 代码领域

在ECMAScript代码执行之前,必须将其与一个领域相关联。从概念上讲,领域由一组内置对象、一个ECMAScript全局环境、在该全局环境的范围内加载的所有ECMAScript代码以及其他相关状态和资源组成。

在实际执行任何代码之前,你编写的每个JavaScript/ECMAScript代码部分都与一个领域相关联。每个领域包括与领域相关联的特定代码部分使用的内在值、领域的this对象、领域的词法环境等等。
这意味着在执行之前会分析您代码的每个词法部分。然后创建一个领域,其中包含有关该代码集的所有信息。源代码、执行所需的变量、已声明的变量、this是什么等等。对于var声明,创建一个领域,在此处定义函数时也是如此:
function do_something() {
  console.log(bar); // undefined
  var bar = 111;
  console.log(bar); // 111
}

在这里,一个函数声明创建了一个与新领域相关联的新词法环境。当创建词法环境时,解释器会分析代码并查找所有声明。然后,在该词法环境的开头首先处理这些声明,因此是函数的“顶部”:
13.3.2 变量声明 var语句声明作用于正在运行的执行上下文的VariableEnvironment的变量。当它们所在的词法环境被实例化时,Var变量被创建,并在创建时初始化为undefined。 因此,每当词法环境被实例化(创建)时,所有var声明都会被创建并初始化为undefined。这意味着它们在任何代码被执行之前在词法环境的“顶部”被处理:
var bar; //Processed and declared first
console.log(bar);
bar = 111;
console.log(bar);

然后,在分析了您的所有JavaScript代码之后,它最终被执行。由于声明首先被处理,因此它被声明(并初始化为undefined),从而使您获得undefined。
提升实际上有点误导性。提升意味着声明直接移动到当前词法环境的顶部,但实际上在执行之前会进行代码分析;没有任何东西被移动。
注意:`let`和`const`的行为方式相同,并且也会被提升,但是以下代码将不起作用:
function do_something() {
  console.log(bar); //ReferenceError
  let bar = 111;
  console.log(bar);
}

这将导致ReferenceError,因为尝试访问未初始化的变量。尽管letconst声明被提升,规范明确指出在初始化之前不能访问它们,不像var

13.3.1 Let and Const Declarations

letconst 声明定义了作用域为运行执行上下文的词法环境的变量。当包含词法环境实例化时创建这些变量,但在变量的词法绑定求值之前无法以任何方式访问。

因此,在正式初始化之前,您无法访问该变量,无论是未定义还是其他任何值。这意味着您无法像使用var那样似乎“在声明之前访问它”。

你认为作用域和词法环境是同一回事吗? - marco

3
"解释型"并不是你想象中的意思。
实际上,这里的"解释型"更像是"按需编译",而不是像你认为的那样逐行编译,它是按可执行代码的单元进行编译的。这些单元首先读入内存,然后在以后被执行。
正是在这些阶段,执行上下文的范围变得已知,声明被提升,标识符被解析。
所有这些的具体实现方法都没有标准化,每个供应商可以自由地实现它们。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接