【问题标题】:Why and when does an immediately ignored variable declaration take hold, and overrides a function declaration in subsequent inquiries?为什么以及何时会立即忽略变量声明,并在后续查询中覆盖函数声明?
【发布时间】:2019-12-28 16:30:50
【问题描述】:

在关注 Kyle Simpson 在Pluralsight 上的 Advanced JavaScript 时,我遇到了这段代码,它应该证明函数声明在之前变量声明:

foo();
var foo = 2;
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }

(请注意,上面的代码要么输入为带空格而不是换行符的单行,要么在输入整个代码之前使用SHIFT+ENTER防止立即执行。)输入整个的立即结果 上面在 Node 或 (Chrome) 控制台中的代码(并按 Enter 键)是:

  foo                                                                                                   
< undefined  

套用 Kyle 的解释,第一个 function foo 声明被第二个覆盖,所以 foo 输出到控制台,并且由于 foo 已经声明为函数,var foo 声明被 (预)编译器。

直接的结果支持了被忽略的理论,然而,随后对foofoo() 的调查显示了一个不同的故事:

> foo                                                                                                   
2                                                                                                       
> foo()                                                                                                 
Uncaught TypeError: foo is not a function                                                               
> 

有人能解释一下为什么以及什么时候var foo = 2; 的被忽略声明在立即执行时产生:

  foo
< undefined  

我的理解是,JavaScript 引擎(预)编译解析步骤应该注意两个函数的声明,然后是变量的声明,按顺序,然后,在执行步骤,foo(); 执行尝试应该失败,因为随后它会使用:Uncaught TypeError: foo is not a function - 但显然情况并非如此,因为foo 将输出作为即时结果的一部分。

【问题讨论】:

  • 我不清楚你为什么认为这与解释有冲突。
  • 您何时输入foo 并获得undefined?控制台不是检查提升的好地方,因为它无法在你输入之前知道你要输入什么,但提升依赖于 JavaScript 引擎预先扫描整个范围。
  • @jonrsharpe 没有立即结果的冲突,正如凯尔所解释的那样。然而,随后对 foo 和 foo() 的调查,实际上并没有被 Kyle 讨论,这只是我的“好奇/勤奋:)”表明最终会发生其他事情,这导致随后的结果与结果相矛盾立即执行。
  • @jonrsharpe - 继续:立即执行:1) foo 是一个函数,因此被执行,2) var foo 声明被忽略。随后的结果: foo 是一个(非函数)2 号引用变量 - 所以它显然以某种方式被声明和分配(不被忽略) - foo() 因此突然抛出异常。这是 JavaScript 语言规范预期的行为,还是一个错误? V8发动机规格如何?有人知道吗?
  • "由于 foo 已被声明为函数,var foo 声明被(预)编译器忽略" - 仅指 var关键字,这意味着它不会再次单独声明(或抛出关于重新声明变量的错误,就像 let 会)。但是在执行代码时,foo = 2 初始化仍然像赋值一样运行。

标签: javascript


【解决方案1】:

javascript中的变量和函数提升

here所写:

提升是一种 JavaScript 机制,其中变量和函数声明在代码执行之前被移动到其作用域的顶部。不可避免地,这意味着无论函数和变量在哪里声明,它们都会被移动到其作用域的顶部,而不管它们的作用域是全局的还是局部的

这意味着在你写这个之后:

foo();
var foo = 2;

function foo() {
  console.log("bar");
}

function foo() {
  console.log("foo");
}

它实际上看起来像这样(这就是为什么事情会以这种方式发生的逻辑:

var foo;

function foo() {
  console.log("bar");
}

function foo() {
  console.log("foo");
}

foo(); // this happens first
foo = 2; // this happens after

console.log(foo);

【讨论】:

  • 其实套用 Kyle 的话,提升只是对 JavaScript 引擎的(预)编译步骤的“简单化”解释,这是 JavaScript 社区神话中广泛接受的,方便解释(预编译)解析步骤,在实际执行之前记录所有声明并从代码中编译出来。
  • 从 MDN 提升文章中也可以清楚地看到,它实际上是一个神话概念:“提升被认为是一种思考执行上下文(特别是创建和执行阶段)的一般方式在 JavaScript 中工作。”
【解决方案2】:

给你两个答案:

  1. 为什么您最初会在控制台中看到undefined,并且

  2. 详细代码中发生了什么

为什么你会看到undefined

如果您的意思是复制整个内容并一次将其全部粘贴到控制台中,如下所示:

> foo();
变量 foo = 2;
函数 foo() { console.log("bar"); }
函数 foo() { console.log("foo"); }

您会看到像这样的输出 fooundefined

然后你输入foo 并得到2

> 富

第一个undefined的原因是它是var语句的结果;语句具有结果值,即使您不能在代码中使用它们,并且控制台通常会向您显示这些结果值。试试吧:

var foo = 2;

您还会看到undefined 作为结果。

那个undefined与吊装无关。

我强烈建议您不要使用控制台来测试与提升相关的内容。控制台是不寻常的环境。如果您想测试类似的东西,请改用 IDE 和/或浏览器中内置的脚本和调试器,设置断点并在您感兴趣的时间检查范围的内容。

详细代码中发生了什么

关于这段代码实际上在做什么:

foo();
var foo = 2;
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }

假设这是在全局范围内,您从 InitializeHostDefinedRealm 的规范开始,它创建全局执行上下文并使用 SetRealmGlobalObject 创建全局环境对象(不是全局对象,浏览器提供)等.(在某处的评论中,您说人们应该解释在哪里保留了内存等等。规范将其留给实现,但创建这些环境对象含糊地关闭。)然后你会再次拿起在ScriptEvaluation 获取包含该代码的脚本,该脚本调用GlobalDeclarationInstantiation,其中包含您似乎感兴趣的内容。(如果该代码在函数中,您将从standard [[Call]] operation 开始,跟随它到@ 987654328@,然后你会在 FunctionDeclarationInstantiation 中找到肉,这与 GlobalDeclarationInstantiation 所做的代码基本相同。)

查看 GlobalDeclarationInstantiation (随着规范的发展,步数会慢慢变质;不过事物的名称通常相当稳定)

  • 在第 7 步中,引擎在脚本的顶层构建“VarScopedDeclarations”列表 (varDeclarations),其中包括 var 声明和函数声明。在您的示例中,varDeclarations 将包括:
    • foovar 变量),
    • foo(第一个foo函数),以及
    • foo(第二个foo函数)
  • 在第 8 步中,它会创建要初始化的函数的空白列表,functionsToInitialize
  • 在第 9 步中,它会创建一个声明函数名称的空白列表,declaredFunctionNames
  • 在第 10 步中,它以 reverse order 执行 varDeclarations
    • 如果条目是 var 声明(或其他几个类似的绑定),则此循环将忽略它,因为此循环只关心函数。
    • 如果条目用于函数:
      • 如果函数的名称不在 declaredFunctionNames 中,引擎会声明该函数,将名称添加到 declaredFunctionNames,然后将该函数插入到 functionsToInitialize em> 开头。
    在您的代码中,引擎从第二个 foo 函数声明(varDeclarations 中的最后一项)开始,在 declaredFunctionNames 中看不到 foo,因此它声明该函数并将其放在列表中以进行初始化。在第二次通过时,foo 已经在列表中,因此引擎不会对其进行任何处理。在第三遍中,foo 是一个 var 声明,因此它不会对它做任何事情。
  • 在第 11 步中,它创建了一个 declaredVarNames 的空白列表。
  • 在第 12 步中,它再次循环遍历 varDeclarations,这次只查看 var 声明,而不是列表中的两个函数声明。如果var 名称不在declaredFunctionNames 中,引擎会创建一个全局变量。但在您的代码中,foo declaredFunctionNames 中,所以它被跳过了。
  • 在步骤 17 中,引擎循环通过 functionsToInitialize,初始化它们。在您的代码中,这会初始化第二个 foo 函数,该函数已在第 10 步(此列表中的第四个项目符号)中放入列表中。

此时,函数foo(第二个)已创建并与"foo" 的全局绑定相关联,并且var foo = 2;var foo 部分已被跳过,因为var 是由函数声明取代。现在是评估该代码主体的时候了:

  • foo() 调用第二个foo 函数,输出"foo"
  • 评估var foo = 2; VariableStatement。当然,var 部分已经完成(在本例中已跳过),因此此时评估的只是 foo = 2 部分,将值 2 分配给全局 "foo" 绑定。 VariableStatement 的结果是一个空完成,控制台显示为 undefined(即使您不能在代码中使用该结果)。 (如果你想向自己证明undefined 来自VariableStatement,只需删除var 并将结果粘贴到控制台。你会看到2 [赋值语句的结果] 而不是undefined。)

代码运行完毕后,"foo" 全局绑定的值为2,因为var 语句中的初始化覆盖了之前分配给绑定的函数。

纵观全局,如果我们删除那些将被 JavaScript 引擎忽略或跳过的内容,并且如果我们重新排序它们以便按照它们出现的顺序列出它们,那么该代码在功能上与以下内容相同:

function foo() { console.log("foo"); }
foo();
foo = 2;

...当然,除了赋值语句foo = 2; 的结果是2,而变量语句var foo = 2; 的结果是undefined。您只能在控制台或类似设备中看到这种差异,但在代码中看不到。

【讨论】:

  • 感谢您详细介绍我们的规范。我有一个关于在GlobalDeclarationInstantiation (GDI) 和FunctionDeclarationInstantiation (FDI) 算法中处理块级变量语句的问题。由于 GDI 的第 7 步(和 FDI 的第 10 步)仅识别 TopLevelVarScopedDeclarations),块内的变量语句和函数声明如何“提升”到封闭的全局(或函数范围,如果是 FDI)?
  • @user51462 - 很高兴有帮助!我不认为 GDI 中的第 6 步(不是当前规范中的第 7 步)/FDI 中的第 10 步仅识别顶级 var 范围声明。使用的规范操作是VarScopedDeclarations,而不是TopLevelVarDeclaredNames。也许我误解了这个问题?
  • 感谢您如此迅速地跟进。我不确定,但SDT for VarScopedDeclarations 似乎暗示它返回TopLevelVarScopedDeclarations
  • @user51462 - 恐怕我有几个项目同时出现,所以我目前无法深入了解规范。如果你弄清楚了,请告诉我。 :-) This may help 如果您已经熟悉它。
【解决方案3】:

没有什么会被忽略,“预编译”一词并不是您在此处看到的。你遇到的是“hoisting”。

所有声明(变量和函数)都被提升到其封闭块的顶部,因此从执行的角度来看,代码被处理为:

var foo;  // The declaration gets hoisted
function foo() { console.log("bar"); }
function foo() { console.log("foo"); }
foo();
foo = 2;  // But not the assignment

重要的是要了解只有声明被提升,而不是赋值,所以foo = 2 的赋值不会被提升并且发生在其他所有事情之后(请参阅“只有声明是我在上面分享的链接中的“吊起”部分)。

至于您在控制台中看到undefined,这只是控制台的一种说法,即您执行的操作没有返回值(与结果不同)。 声明语句的var foo 没有返回值或结果,因此控制台会显示undefined。如果您只是在控制台中输入:var x = 10;,您会看到相同的 undefined 响应。

【讨论】:

  • 其实你的大胆点可能是OP的困惑,我不知道。
  • 其实套用 Kyle 的话说,提升只是对 JavaScript 引擎的(预)编译步骤的“简单化”解释,这是 JavaScript 社区神话中广泛接受的,方便解释(预编译)解析步骤,在实际执行之前记录所有声明并从代码中编译出来。
  • @LuciferMorningstar 客户端如何处理和优化 JavaScript 的内部工作实际上由客户端决定,因此“预编译”一词不能保证 100% 准确。但是,在客户端实施过程中发生的事情才是最重要的,而“提升”就是您在此处看到的内容。
  • “至于您在控制台中看到 undefined,这只是控制台的一种说法,即您执行的操作没有返回值……” 不完全是。大多数控制台(至少在浏览器中)会显示语句的result,即使您不能在代码中使用语句的结果。例如,尝试:{ "foo"; },这是一个包含单个表达式语句的块语句。大多数控制台都会向您显示该块语句 ("foo") 的结果,即使您不能在代码中使用这些结果。在这种情况下,我怀疑这是var 语句的结果。 :-)
  • 从 MDN 提升文章中也可以清楚地看到,它实际上是一个神话概念:“提升被认为是一种思考执行上下文(特别是创建和执行阶段)的一般方式在 JavaScript 中工作。”然而,实际上,在创建阶段,变量是按什么顺序创建(并可能被覆盖)的?
【解决方案4】:

由于变量和函数声明get hoisted to the top of the code,您的代码在语义上等同于以下内容:

var foo;
 
function foo() {
  console.log("bar");
}

function foo() {
  console.log("foo");
}

foo();
foo = 2;

第二个函数声明确实覆盖了第一个,这就是为什么在调用foo() 时会在控制台上打印“foo”。

最后一条语句只是用2 而不是函数再次覆盖foo。它不会被忽略,因为 foo 在此之后拥有值 2;它只是在最后评估。

【讨论】:

  • 更准确地说:var foo; function foo() { /*...*/ } function foo() { /*...*/ } foo(); foo = 2;(但问题的重点似乎是在控制台中做事,这不是理解这些东西的好环境。)
  • 其实套用 Kyle 的话,提升只是对 JavaScript 引擎的(预)编译步骤的“简单化”解释,这是 JavaScript 社区神话中广泛接受的,方便解释(预编译)解析步骤,在实际执行之前记录所有声明并从代码中编译出来。
  • @T.J.Crowder 在我的回答中,我评论你说它实际上的行为方式与它在控制台中的行为方式相同。
  • 从 MDN 提升文章中也可以清楚地看到,它实际上是一个神话概念:“提升被认为是一种思考执行上下文(特别是创建和执行阶段)的一般方式在 JavaScript 中工作。”然而,实际上,在创建阶段,变量是按什么顺序创建(并可能被覆盖)的?
【解决方案5】:

好的,所以@Bergi 对 OP 的评论清楚地解释了我所询问的行为,我已要求他从中创建一个答案,以便我可以将其标记为已接受。 同时,这里是即时输出的解释,以及后续的查询,据我所知,基于这个讨论:

1) 在 JavaScript ** 创建变量/初始化作用域**(又名(概念/神话)提升)阶段,一个引用 foo 被声明,然后作为一个函数被覆盖,随后的 var foo 声明是忽略。

2) 在随后的 JavaScript 执行阶段:

  • a) foo(); 被执行,此时 foo 是一个函数,所以 结果foo 按照console.log("foo"); 输出到第一行

  • b) foo = 2; 被执行,将 foofunction 重新定义为 number 并为其分配2,同时输出undefined作为结果 整个操作。

就是这样,也可以通过实际阅读 15.1.11 运行时语义:GlobalDeclarationInstantiation ( script, env ) - http://ecma-international.org/ecma-262/10.0/index.html#sec-globaldeclarationinstantiation 标准的一部分来预测。

【讨论】:

  • @Bergi 请查看我根据您对 OP 的评论以及其他部分答案的自我回答,不幸的是,这些回答大多坚持我讨厌的提升术语,并根据您的知识在您的答案中复制/编辑/扩展,以便我接受。
  • 我还是不明白这个答案和其他答案有什么区别
  • 对我来说看起来不错,除了将其称为“编译阶段” - 它实际上只是创建变量/初始化范围 :-) 有关更多详细信息和规范参考,请参阅@TJCrowder 的答案
  • "...同时输出 undefined 作为整个操作的结果。" 排序。这是var foo = 2; VarStatement 的结果,它是代码中执行的最后一件事,因此它是代码中的最后一个结果,也是控制台报告的结果。
【解决方案6】:

首先预编译函数定义,然后解释器将开始执行其他语句。 var foo = 5 将覆盖该函数 - 除非有 "use strict",如果您尝试重新定义名称,它将引发错误。

【讨论】:

  • 不,OP 代码中的任何内容都不会导致严格模式下的错误。
猜你喜欢
  • 1970-01-01
  • 2018-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多