这种提升如何与块作用域一起工作?

How does this hoisting work with block scope?

有人问我一个问题

{
  function foo() {
    console.log('A');
  }
  
  foo();
  
  foo = 1;
  
  function foo() {
    console.log('B');
  }
  
  foo = 2;

  console.log(foo);
}
console.log(foo);

为什么第三个输出是1而不是2


不应该创建范围为 foo 的块,因为该块中既没有 let 也没有 const。但是第二个 foo 输出是 2 意味着确实已经创建了另一个 foo 的引用。

这是怎么回事?

P.S. I'm using Chrome Version 89.0.4389.90 (Official Build) (x86_64).

这是对 Node.js 中调试器所发生情况的分析。它没有解释为什么会这样。

涉及3个作用域:局部作用域、全局作用域和块作用域。

我在 Chrome 浏览器中进行了相同的分析。行为类似,唯一的区别是没有局部作用域,而不是局部作用域,而是使用全局作用域。

密码

{
    foo = 1;
    foo = 2;
    console.log(foo);
}
console.log(foo);

在全局范围内创建一个变量并为该变量设置两个不同的值。在这段代码中

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

function foo() { } 在块作用域中创建了一个变量 foo,在局部作用域中创建了一个变量 foo(Chrome 中的全局作用域)。由于块作用域中存在一个变量 foo = 1; 为块作用域中的现有变量设置一个值,并且不会在全局作用域中创建变量。 foo = 2; 为同一个变量设置不同的值。第一个 console.log(foo); 从块范围打印 2,第二个 console.log(foo); 从本地范围(Chrome 中的全局范围)打印 f foo() { }

在此代码中

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

函数声明 function foo() { } 被提升并在块作用域中创建一个变量 foo,值为 f foo() {},在局部作用域中创建一个变量 foo(全局作用域在 Chrome),值为 undefinedfoo = 1; 行将两个变量设置为 1foo = 2; 行将块作用域中的变量设置为 2。第一个 console.log(foo); 从块范围打印 2,第二个 console.log(foo); 从本地范围(Chrome 中的全局范围)打印 1

在此代码中

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

函数声明 function foo() { } 在块作用域中创建一个变量 foo,值为 f foo() {},在局部作用域中创建一个变量 foo([=94= 中的全局作用域) ]) 的值为 f foo() {}foo = 1; 行将两个变量设置为 1foo = 2; 行将块作用域中的变量设置为 2。第一个 console.log(foo); 从块范围打印 2,第二个 console.log(foo); 从本地范围(Chrome 中的全局范围)打印 1


根据函数声明处的web compat semantics,阻塞作用域变量的值绑定到外部作用域²。此代码等效于:

let outerFoo; // the functions create a binding outside of the scope

{
  let innerFoo; // but also inside
  // due to hoisting, functions get bound before any code get's executed:
  innerFoo = function foo() {
    console.log('A');
  };
  innerFoo =   function foo() {
    console.log('B');
  };
  
  // At the place of the function declaration, the variable leaves the scope
  /* function foo() {
    console.log('A');
  } */
  outerFoo = innerFoo;

  innerFoo();
  
  innerFoo = 1;
  
  // this also applies to the second declaration
  /* function foo() {
    console.log('B');
  } */
  outerFoo = innerFoo;
  
  innerFoo = 2;

  console.log(innerFoo);
}
console.log(outerFoo);

²这基本上就是规范描述的方式:

When the FunctionDeclaration f is evaluated, perform the following steps in place of the FunctionDeclaration Evaluation algorithm provided in 15.2.6:
a. Let fenv be the running execution context's VariableEnvironment.
b. Let benv be the running execution context's LexicalEnvironment.
c. Let fobj be ! benv.GetBindingValue(F, false).
d. Perform ! fenv.SetMutableBinding(F, fobj, false).

规范还指出:

Prior to ECMAScript 2015, the ECMAScript specification did not define the occurrence of a FunctionDeclaration as an element of a Block statement's StatementList. However, support for that form of FunctionDeclaration was an allowable extension and most browser-hosted ECMAScript implementations permitted them. Unfortunately, the semantics of such declarations differ among those implementations. Because of these semantic differences, existing web ECMAScript code that uses Block level function declarations is only portable among browser implementation if the usage only depends upon the semantic intersection of all of the browser implementations for such declarations

所以 Safari 可能会按照它一贯的方式来做,而 Chrome(和 Firefox)遵循规范。