为什么即使我在 2 个不同的范围内声明变量仍然存在重复错误?

Why is there still a duplicate error even when I declare the variable in 2 different scopes?

function f1(x = 2, f = function() {x = 3;}) {
  let x = 5;
  f();
  console.log(x);
}
f1();

在这段代码中有一个语法错误,表示 Identifier 'x' has already been declared。很明显,我们不能在一个范围内使用 let 重新声明一个变量。但是我不知道为什么在这段代码中我们仍然会得到这个错误,因为在 ES6 中默认参数实际上会创建另一个名为参数环境的范围。

http://www.ecma-international.org/ecma-262/6.0/#sec-functiondeclarationinstantiation

If the function’s formal parameters do not include any default value initializers then the body declarations are instantiated in the same Environment Record as the parameters. If default value parameter initializers exist, a second Environment Record is created for the body declarations. Formal parameters and functions are initialized as part of FunctionDeclarationInstantiation. All other bindings are initialized during evaluation of the function body.

所以这里我们有一个全局作用域、一个参数作用域和一个函数作用域。在参数范围内,我们声明了一个名为 x 的参数,同时我们还在函数范围内声明了另一个名为 x 的变量。尽管这两个名称相同,但它们存在于不同的范围内。为什么在这种情况下我们仍然会收到提示不允许重复的语法错误?

关于语法错误:参数与函数体在同一范围内。因此,如果您有一个名为 x 的参数,那么在执行函数时,代码实际上是 声明 var x。或者 var x = 2 作为默认参数。 (这可能不是计算机工程师所描述的确切机制,但对于使用代码而言,这基本上就是正在发生的事情)。

如果您将参数 "x" 改为 "z"(见下文),语法错误就会消失。

function f1(z = 2, f = function() {x = 3;}) {
   let x = 5;
   f();
   console.log(x);
 }
 f1();

关于不影响 console.log()'d 的 x 值的 f() 函数。我认为它与声明函数的位置以及函数声明的顺序有关JavaScript 被编译,在调用函数时影响函数的范围。

例如,这段代码不改变x的值:

 function f() {
     x = 3;  
 }

 function f1(z = 2) {
    var x = 5;
    f();
    console.log(x);
  }
  f1();

但这段代码确实如此:

 function f1(z = 2) {
    function f() {
       x = 3;  
    }
    var x = 5;
    f();
    console.log(x);
  }
  f1();

**(如果你down-vote回答了,能解释一下原因吗?)

调用函数时,会创建一个新的执行上下文。每个执行上下文都有自己的本地内存。传递给函数的参数在函数的本地内存中 "saved"。
鉴于此片段:

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

foo(5);

好像代码真的是这样写的:

function foo(x) {
    // var x = 5;
    console.log(x);
}

foo(5);

默认参数 may act a bit differently behind the scenes,标签仍然在局部范围内。
据我所知,他们可能有不同的环境记录,但实际上没有不同的范围块。因此错误,let 变量不能在同一块范围内 re-declare。

我建议阅读 this SO answer 有关范围的内容

是的,你是对的。这里涉及三个不同的范围(一个用于第一个参数,一个用于第二个,一个用于主体)。

然而,在参数被初始化(在它们自己的范围内)之后,它们被复制到一个新的词法环境(然后主体将在其中执行)(可以在规范的 9.2.15 中找到)。

这意味着参数名称不仅存在于参数的范围内,而且还存在于计算主体的范围内,因此在主体内部使用相同的名称是重新声明 的变量,导致错误(let / const)。


这是规范的演练:

当一个函数被解析时,它会创建一个函数对象,其中包含一些内部属性:

[[Environment]]:这是对外部作用域的引用,因此您可以在函数内部访问外部作用域的变量(这也会导致关闭行为,[[Environment]] 可能会引用一个环境不再活跃了)。

[[FormalParameters]]: 参数的解析代码。

[[ECMAScriptCode]]: 函数体代码。

现在当你调用一个函数(9.2.1 [[Call]])时,它会在调用栈上分配一个环境记录,然后

Let result be OrdinaryCallEvaluateBody(F, argumentsList).

调用函数。这就是9.2.15进来的地方。首先,它将在函数体环境中声明所有参数:

[Initialize local helper variables]

21. For each String paramName in parameterNames, do

    i. Perform ! envRec.CreateMutableBinding(paramName, false).

 [Rules for initializing the special "arguments" variable]

然后它将初始化所有参数。参数真的很复杂,因为还有rest参数。因此必须迭代参数才能将它们变成数组:

24. Let iteratorRecord be CreateListIteratorRecord(argumentsList)

25. If hasDuplicates is true, then

   a. Perform ? IteratorBindingInitialization for formals with iteratorRecord and undefined as arguments.

26. Else,

   a. Perform ? IteratorBindingInitialization for formals with iteratorRecord and env as arguments.

现在IteratorBindingInitialization定义在13.3.3.8:

它基本上会评估默认值,并会在当前环境中初始化绑定。

当所有参数都初始化后,函数体的环境就可以准备好了。正如您引用的评论中提到的,如果没有参数初始值设定项,则不需要新的环境。但是,如果某处有默认值,则会创建一个新环境。

27. If hasParameterExpressions is false, then

 [...]

 28. Else,

     a. NOTE: A separate Environment Record is needed 
             to ensure that closures created by expressions in the
              formal parameter list do not have visibility of 
              declarations in the function body.

然后创建一个 "new scope",在其中评估正文:

    b. Let varEnv be NewDeclarativeEnvironment(env).

    c. Let varEnvRec be varEnv's EnvironmentRecord.

    d. Set the VariableEnvironment of calleeContext to varEnv.

然后函数的所有变量和参数都在该范围内声明和初始化,这意味着所有参数都得到复制:

   f. For each n in varNames, do

     2. Perform ! varEnvRec.CreateMutableBinding(n, false).

     3. If n is [a regular variable declared with "var"], let 
        initialValue be undefined.

     4. Else, [if it is a parameter]

         a. Let initialValue be ! envRec.GetBindingValue(n, false)

     5. Call varEnvRec.InitializeBinding(n, initialValue).

 [Other rules for functions in strict mode]

 31. [varEnv gets renamed to lexEnv for some reason]

之后,声明所有let / const的内部变量(但未初始化,它们将在达到let时初始化)。

34. Let lexDeclarations be the LexicallyScopedDeclarations of code.

35. For each element d in lexDeclarations, do

     a. NOTE: A lexically declared name cannot be the 
                  same as a function/generator declaration, formal
                   parameter, or a var name. Lexically declared 
                   names are only instantiated here but not initialized.

     b. For each element dn of the BoundNames of d, do

        i. If IsConstantDeclaration of d is true, then

          1. Perform ! lexEnvRec.CreateImmutableBinding(dn, true).

       ii. Else,

         1. Perform ! lexEnvRec.CreateMutableBinding(dn, false).

现在如您所见,CreateMutableBinding 在这里被调用,并且如 8.1.1.1.2 中所指定...

The concrete Environment Record method CreateMutableBinding for declarative Environment Records creates a new mutable binding for the name N that is uninitialized. A binding must not already exist in this Environment Record.

所以参数已经复制到当前环境中,调用同名CreateMutableBinding会失败