ES2015 中的 switch 语句和作用域

switch statement and scopes in ES2015

考虑这个 ES2015 模块和节点 v4.4.5 中 运行 时的行为。

'use strict'

const outer = 1

switch ('foo') {
  case 'bar':
    const heyBar = 'HEY_BAR'
    break
  case 'baz':
    const heyBaz = 'HEY_BAZ'
    break
  default:
    const heyDefault = 'HEY_DEFAULT'
}
console.log(
  outer, // 1, makes sense, same top-level scope
  heyBar, // undefined. huh? I thought switch did NOT create a child scope
  heyBaz, // undefined. huh? I thought switch did NOT create a child scope
  heyDefault) // 'HEY_DEFAULT' makes sense

这对我来说似乎内部不一致。如果 switch 语句没有创建词法作用域,我希望所有 hey* 变量都成为主作用域的一部分并且所有行为都一致。如果 switch 语句确实创建了一个词法作用域,我仍然希望它们是一致的,但是 case 子句中声明的变量表现得就像它们在子作用域中一样,而 default 中的变量子句的行为就像在外部范围内一样。

我的问题是 switch 语句中是否涉及任何子范围,如果有,它们的行为细节是什么?

在节点 v6.4.0 中,行为有所不同。看起来 switch 块确实创建了一个子块作用域。

ReferenceError: heyBar is not defined

这似乎更容易理解。

我完全无法重现你的行为。我立即得到一个 ReferenceError(节点 6.4.0 和当前的 Firefox):

ReferenceError: heyBar is not defined

这对我来说似乎是正确的行为。 AFAIK switch 带方括号的语句确实会创建一个块,从而为块作用域的实体创建一个词法作用域。 case 语句本身不会创建自己的块。

如果我们在 switch 语句中使用 foo 情况扩展此示例,它也会抛出 ReferenceError

'use strict'

const outer = 1

switch ('foo') {
  case 'bar':
    const heyBar = 'HEY_BAR'
    break
  case 'baz':
    const heyBaz = 'HEY_BAZ'
    break
  case 'foo':
    const heyFoo = 'HEY_FOO'
    break
  default:
    const heyDefault = 'HEY_DEFAULT'
}
console.log(
  outer,
  heyFoo,
  heyBar,
  heyBaz,
  heyDefault) // ReferenceError: heyFoo is not defined

参考

Here is the section in the spec: 13.12.11 Runtime Semantics: Evaluation

5. Let blockEnv be NewDeclarativeEnvironment(oldEnv).
6. Perform BlockDeclarationInstantiation(CaseBlock, blockEnv).

其中CaseBlock是switch case的块语句。

这大致转化为:

在switch语句(switch { <-here-> })的块中创建一个新的块环境并实例化所有块级声明(如letconst或块级函数声明) .

如果没有heyBar,上面的代码在严格模式下抛出ReferenceError: heyDefault is not defined,否则抛出ReferenceError: heyBar

switchswitch (...) { ... } 语句创建范围,不创建 case 语句的范围。参见 the reference

switch 语句的主体创建了一个新的块作用域。每个单独的 case 子句或 default 子句不会自动创建新的块作用域。

理解作用域和 switch 语句的权威参考当然是 ES2016 specification。然而,要弄清楚它真正在说什么还需要做一些工作。我将尝试向您介绍我可以从该规范中遵循的内容。

定义重要术语

首先,switch 语句定义为:

SwitchStatement:
    switch ( Expression ) CaseBlock

而且,CaseBlock 是这样的:

CaseBlock:
    { CaseClauses }
    { CaseClauses DefaultClause CaseClauses }

CaseClauses:
    CaseClause
    CaseClauses CaseClause

CaseClause:
    case Expression : StatementList

DefaultClause:
    default : StatementList

所以 CaseBlockswitch 语句的主体(包含所有情况的代码)。

这是我们所期望的,但是上面定义的术语 CaseBlock 对于其他规范参考很重要。

CaseBlock 创建新范围

然后,在 13.2.14 Runtime Semantics: BlockDeclarationInstantiation( code, env ) 中,我们可以看到 CaseBlock 导致创建了一个新的作用域。

When a Block or CaseBlock production is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, generator function, or class declared in the block are instantiated in the Environment Record.

由于 CaseBlockswitch 语句的主体,这意味着 switch 语句的主体创建了一个新的块作用域(新 [=85 的容器) =]声明)。

CaseClause 添加到现有范围(不创建自己的范围)

然后,在 13.12.6 Static Semantics: LexicallyScopedDeclarations 中,它描述了解释器在解析时如何收集词法范围的声明。这是规范中的实际文本(解释如下):

CaseBlock : { CaseClauses DefaultClause CaseClauses }

  1. If the first CaseClauses is present, let declarations be the LexicallyScopedDeclarations of the first CaseClauses.
  2. Else let declarations be a new empty List.
  3. Append to declarations the elements of the LexicallyScopedDeclarations of the DefaultClause.
  4. If the second CaseClauses is not present, return declarations.
  5. Else return the result of appending to declarations the elements of the LexicallyScopedDeclarations of the second CaseClauses.

CaseClauses : CaseClauses CaseClause

  1. Let declarations be LexicallyScopedDeclarations of CaseClauses.
  2. Append to declarations the elements of the LexicallyScopedDeclarations of CaseClause.
  3. Return declarations.

CaseClause : case Expression : StatementList

  1. If the StatementList is present, return the LexicallyScopedDeclarations of StatementList.
  2. Else return a new empty List.

DefaultClause : default : StatementList

  1. If the StatementList is present, return the LexicallyScopedDeclarations of StatementList.
  2. Else return a new empty List.

所以,基本上这就是说第一个 caseClause 创建了一个 LexicallyScopedDeclarations 对象。并且,随后的每个 DefaultClause 或 CaseClause 都会附加到该声明对象。这就是规范描述在一个范围内构建所有声明的方式。

A CaseClause 追加到现有的声明对象,它不会创建自己的。这意味着它不会创建自己的作用域,而是使用包含作用域。

当然,您可以在 CaseClause 中定义一个块,然后该块将成为它自己的范围,但是 CaseClause 不需要块声明,因此它不需要,默认情况下,创建一个新范围。

运行时语义:求值

然后,在 13.12.11 Runtime Semantics: Evaluation

中进一步解释了运行时的工作原理

SwitchStatement: switch(Expression) CaseBlock

  1. Let exprRef be the result of evaluating Expression.
  2. Let switchValue be ? GetValue(exprRef).
  3. Let oldEnv be the running execution context's LexicalEnvironment.
  4. Let blockEnv be NewDeclarativeEnvironment(oldEnv).
  5. Perform BlockDeclarationInstantiation(CaseBlock, blockEnv).
  6. Set the running execution context's LexicalEnvironment to blockEnv.
  7. Let R be the result of performing CaseBlockEvaluation of CaseBlock with argument switchValue.
  8. Set the running execution context's LexicalEnvironment to oldEnv.
  9. Return R.

此处的操作步骤是第 4 步和第 5 步,其中为 CaseBlock 创建了一个新的块环境。如果您继续阅读 13.12.11 的文本,则不会为 CaseBlock.

中的 CaseClause 创建新的块环境

CaseClause: case Expression: StatementList

  1. Return the result of evaluating StatementList.

所以,你有它。 CaseBlock 创建一个新的块作用域。 CaseClause 不会(除非您自己在 CaseClause 中明确定义一个块)。

使用{ },封装每个包含作用域声明的case以创建新的块作用域:

const var1 = true;

switch (var1) {
  case true: {
    const var2 = 0;
    break;
  }
  case false: {
    const var2 = 1;
    break;
  }
  default: {
    const var2 = 2;
  }
}