函数声明的评估顺���

5
使用以下代码,它既不是ES6,也不在“严格模式”中,我原本预期的结果应该是'b',因为foo的第二个声明应该覆盖第一个声明。但结果却是'a'!
{
  function foo() {
    console.log('a');
  }
}

function foo() {
  console.log('b');
}

foo(); // 'a' ? Why not 'b'?

当这段代码被额外的花括号包围时,结果会是预期的 'b'。
{ // additional curly braces

  {
    function foo() {
      console.log('a');
    }
  }

  function foo() {
    console.log('b');
  }

  foo(); // 'b' as expected!

} // end additional curly braces 

请参考下面的附加示例以获得进一步说明:
foo('before declaration'); // outcome:  from outside block :before declaration

{
  function foo(s) {
    console.log('from inside block: ' + s);
  }
}

function foo(s) {
  console.log('from outside block :' + s);
}

foo('after declaration'); // outcome:  from inside block: after declaration

在我看来,正确的结果应该是:

// from outside block :before declaration
// from outside block :after declaration

我无法找到我的误解所在。

如果我再次将完整的最后一个例子放在花括号中,就像这样:

{
  foo('before declaration'); // outcome:  from outside block :before declaration

  {
    function foo(s) {
      console.log('from inside block: ' + s);
    }
  }

  function foo(s) {
    console.log('from outside block :' + s);
  }

  foo('after declaration'); // outcome:  from outside block: after declaration
}

我得到了预期的结果。


@klugjo - 用哪个浏览器? - Jaromanda X
1
如果您使用 "use strict",请注意差异。 - Jaromanda X
真的吗?也许你把"use strict"放错了位置 :p 请比较https://jsfiddle.net/2jytw2mq/和https://jsfiddle.net/2jytw2mq/1/。 - Jaromanda X
我希望重复说明得足够清楚,这是为什么会在 Web 兼容模式下发生 - 块中的声明将在计算时覆盖外部(函数作用域)变量。 要修复此行为,请使用严格模式。 - Bergi
@Bergi 对不起,您没有回答我的问题。不幸的是,我无法在您的答案中识别出一致的逻辑,特别是与我的扩展示例(第三部分)的关系。也许我的问题是错的。我想我必须自己找到正确的答案。一切都好,祝您有愉快的一天。 - micmor
显示剩余8条评论
3个回答

1
在你的第一个声明中,你将foo()包含在{}中,这改变了声明的作用域,但下一个声明是全局作用域。根据w3schools JavaScript Hoisting

Hoisting是JavaScript的默认行为,将所有声明移动到当前作用域的顶部

因此,你的第二个声明实际上被移动到了顶部,然后执行了作用域内的声明{},覆盖了第一个声明并打印出a

2
为什么这不适用于我代码示例的第二部分呢?这就是让我困惑的地方。 - micmor
@slebetman 当然可以 - Bergi
即便这是您本意,现在发生的事情也不是这样的。a) 函数按照出现的顺序进行提升,第一个会被第二个覆盖而不是相反;b) 函数在它们自己的作用域中进行提升,在此示例中,该作用域为一个函数的块级作用域。在提升期间,它们不会发生冲突。 - Bergi
@sSD 如需进一步了解,请查看我对重复问题的回答 :-) - Bergi
@micmor 在严格模式下(通常应该使用),确实是这种情况。为什么在松散模式下你仍然可以访问函数超出它们通常的作用域,这在我的回答中有解释。 - Bergi
显示剩余6条评论

1
我原本期望的结果是'b',因为foo的第二个声明应该会覆盖第一个。
不是因为第二个声明应该覆盖第一个。但期望的结果将是“b”,因为第一个声明包含在其自己的(块)作用域中,不会影响外部作用域。
但结果却是“a”!为什么不是“b”?因为您没有以严格模式执行代码(您确实应该这样做!)。在松散模式下,网络浏览器会 表现奇怪,以兼容对ES5中非法函数语句进行特殊处理的旧引擎。
那么这里发生了什么?当评估该语句时,块作用域的函数声明确实将该函数赋值给相同名称的顶级变量。(除了正常的作用域(和“提升”)声明外,还会发生这种情况)。
因此,您的第一个示例的行为类似于:
var foo₀; // the top-level implicit introduction of the inner declaration
var foo₀ = function foo() { console.log('b'); } // the hoisted declaration
foo()₀; // logs 'b'
{
  let foo₁ = function foo() { console.log('a'); } // the (hoisted) inner declaration
  foo₀ = foo₁; // the weirdness!
  // a call to `foo()` in here would get you foo₁ from the local scope
}
foo()₀; // logs 'a'

你的第二个例子表现为

var foo₀; // the top-level implicit introduction of the first declaration
var foo₀; // the top-level implicit introduction of the second declaration
{
  let foo₁ = function foo() { console.log('b'); } // the (hoisted) second declaration
  foo()₁; // logs 'b'
  {
    let foo₂ = function foo() { console.log('a'); } // the (hoisted) first declaration
    foo₀ = foo₂; // the weirdness!
    // a call to `foo()` in here would get you foo₂ from the local scope
  }
  foo₀ = foo₁; // the weirdness!
  foo₁() // logs 'b' (as expected) - a call in here does get you foo₁ from the local scope
}
// a call to `foo()` out here would get you foo₀ from the top scope

1
谢谢您的详细解释。这样写,问题对我来说非常清楚了。奇怪的行为是由于松散模式造成的,因此缺乏一致的逻辑。 - micmor
我有一个问题是“因为第一个声明包含在它自己的(块)作用域中,不会影响外部作用域。”这句话正确吗?因为functionvar存在于父函数或文件的作用域中,而不是由大括号定义的块中。但对于letconst来说就不同了,它们在{}块之外不存在。实际上,{function foo(){}}使得foo()存在于文件/父函数中。 - E. Zacarias
1
@E.Zacarias 不是的,function声明存在于块级作用域中。如果您没有使用严格模式(尽管您真的应该!),它们还会引入额外的全局/函数作用域变量。引擎需要这样做是为了提供Web兼容性,以支持依赖奇怪变量提升行为的遗留代码。 - Bergi

-1
在单个作用域中具有同一函数的多个定义是不受支持的,除非它们位于顶层。
原因是例如以下两者之间存在很大差异:
function square(x) { return x*x; }

并且

var square = function(x) { return x*x; }

在第一种情况下,名称square绑定到开始时的函数对象,而不是在正常程序流程中“执行”定义时:

function foo() {
    console.log(square(12));  // Valid, no prob the function is defined "later"
    function square(x) { return x*x; }
}

这意味着如果您在同一作用域中放置不同的定义,则不清楚应该执行什么操作。

标准仅描述了多个顶级定义的情况下应该发生什么(最后一个胜出)。

相反,在嵌套部分中应该做什么没有定义,不同的实现(例如Firefox和Chrome)并不一致。显然是荒谬的,例如在if-else语句的两个分支中放置同一函数的两个定义(请记住,名称绑定必须立即发生,而不是在作用域中的语句开始执行之前)。

千万不要这样做。


1
实际上,我测试过的所有浏览器和Node.js都表现出相同的行为 - 因此,“不同的实现以不同的方式处理这个问题”似乎更像是一种观点而不是事实 :p - Jaromanda X
说实话,我不确定你的平方函数如何证明任何与问题相关的内容。 - Jaromanda X
函数 foo() { console.log('a'); }函数 foo() { console.log('b'); }foo(); // b - micmor
在实践中,我从未做过这样的事情。但是理论上,允许重复的函数声明,并且最后一个应该覆盖任何先前的声明。为什么我的代码示例不是这种情况呢?周围的花括号不应该改变作用域!! 显然我没有完全理解这个机制! - micmor
@micmor:标准仅描述了顶层的多个定义应该如何处理。嵌套部分的处理方式没有被定义,不同的实现(如Firefox和Chrome)之间存在不一致。显然无意义的是,在if语句的两个分支中放置相同函数的两个定义(请记住,名称绑定必须在语句执行之前立即发生)。 - 6502
显示剩余4条评论

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