如何理解 let x = x 报错之后,再次 let x 依然会报错?

不需要分析为什么 let x = x 会报错,前面两个报错我理解,请分析后面两个报错。 [图片] 感觉 x 被玩坏了… P.S. 上面是 Chrome…
关注者
116
被浏览
30,183

12 个回答

let x = x 扔出的错误乃右边的表达式求值时产生运行时错误,但是 let x 本身是有效的。

实际上如果不是在console里一句一句执行,还没等扔运行时错误(ReferenceError)第二个let x语句就首先产生了早期错误(SyntaxError)。


【补充】

@萧井陌 的答案提到 console 特殊环境,确实,这里与 console 的 repl 实现有一定的关系,下面 @余博伦 答案也指出 edge 的 console 下结果就不同。不过 edge 的问题其实是在 repl 下每次执行代码中的 let 定义只对当次的代码有效,即其每次执行实际上是在一个单独 scope 里。这是 edge devtools 的一个缺陷。

但是注意,这个例子并不是“不存在的粪坑”。

通常,let 所在的 block 一旦发生错误,你是没办法重新继续执行该 block 内 let 语句之后的语句的。你也许想用 try catch,但 try 自己就形成了一个 block,所以此路不通。

但是有一个例外,那就是浏览器里的 global script 代码,其 let 定义是跨 script 块有效的。(node.js 因为每个模块本质上是包装过的函数,所以是无法实现的。)并且一个 script 块报错后面的 script 块会继续执行。

代码如下:

<!doctype html>
<script>
function throws() { throw '!' }
let x = throws()
</script>
<script>
console.log(typeof x)
</script>
<script>
let x
</script>

这里我用 let x = throws() 代替了 let x = x,是因为这里只需要扔出运行异常,而不必是引用自身这种特例。

第一个script跑完之后,x 虽然已经存在了,但是处于未初始化的状态,即类似TDZ的情况。之后即使使用 typeof x 也会报错(通常未声明变量会返回 'undefined')。

chrome里报错如下:

test.html:3 Uncaught !
test.html:7 Uncaught ReferenceError: x is not defined
test.html:9 Uncaught SyntaxError: Identifier 'x' has already been declared

edge里报错如下:

!
Use before declaration
Let/Const redeclaration

(注意:edge貌似会并行parse不同的script块,因为第三条是early error,而第二条是运行时错误,所以有时候你会看到后两条报错的顺序是相反的。)

ff里报错如下:

uncaught exception: !
ReferenceError: can't access lexical declaration `x' before initialization test.html:7:13
SyntaxError: redeclaration of let x test.html:9:1

看起来,还是ff的错误信息一如既往的最清楚。

顺便,我发现其实这个问题之前已经有人提过了:js中用let声明变量出错时,变量依然被声明且无法再赋值是什么原理? 并且下面的答案也都比较准确的回答了,所以大家还是要善用搜索。

至少在 V8 的 console 中:

  • Reference Error 可以只抛出一个 Message,不会停止当前语句在当前环境的各种副作用
  • Syntax Error 一定会取消掉当前 statement 的所有作用(Early Error)
  • V8 的错误提示不太清楚

第一行:

x;

x 作为 Identifier Reference(这是一个 Primary Expression),在当前 Lexical Enviroment 和向上的 env 查找,都没有 "x" 这个 Identifier,抛出一个 ReferenceError:

  • V8:x is not defined
  • Nitro:Can't find variable: x
  • SpiderMonkey:x is not defined

第二行:

let x = x;

参考 V8 的 let declaration 实现(简化):

// /v8/src/parsing/parser-base.h
// ParseVariableDeclarations()

// parse let Lexcical Declaration 中的 Lexical Binding
do {
  // Reference 的 name
  // 此处 pattern(bindingId) 为 "x"
  ExpressionT pattern = ParsePrimaryExpression();
  // 把 "x" 作为 identifier 放在当前 parse env 中,即 ecma 中的 create
  // 这会被 Syntax Error 取消掉
  PushVariableName(AsIdentifier(pattern));

  // Reference 的 value
  ExpressionT value = EmptyExpression();
  if (Check(Token::ASSIGN)) {
    // 用 assignment expression 的结果 initialize
    value = ParseAssignmentExpression();
  } else {
    // 没有 Initializer
    // 对于 const 会抛出 Syntax Error;对于 let 会设为 undefined
    value = GetLiteralUndefined();
  }

  // 初始化 Reference
  Declaration decl(pattern, value);
  DeclareAndInitializeVariables(&decl);
} while (Check(Token::COMMA));

x 会先通过 PushVariableName 被声明(declare),但没有被初始化(initialized)。然后在 ParseAssignmentExpression 取右值 x 的时候,会抛出一个 Reference Error:

  • V8:x is not defined
  • Nitro:Cannot access uninitialized variable.
  • SpiderMonkey:can't access lexical declaration 'x' before initialization

Nitro 和 SpiderMonkey 的 Error Message 清晰地表达了这个过程,而 V8 的提示就稍有暧昧。

可以参考下面这句:

let x = y;

抛出的 ReferenceError:

  • V8:y is not defined
  • Nitro:Can't find variable: y
  • SpiderMonkey:y is not defined

虽然在一般的 JS 程序里,这已经足以使当前 running execution context 终止,没有后面的事了。但是实际上,这个 Reference Error 没有使最后的 DeclareAndInitializeVariables 失效,在 console 或者浏览器 Script 环境中,代码可以继续执行。

在 SpiderMonkey 中,x 还会像 let x; 那样以 undefined 初始化,尽量避免出现「没有初始化」的情况(区分「not defined」和「以 undefined 为值初始化」)

第三行:

x;

x 作为 Reference,这个变量在当前 Lexical Environment 中已经存在(created),即有 "x" 作为 Identifier。但是变量没有被初始化(initialized)的话,就既无法被访问(access)、当然也无法被赋值,这是对 es 标准的实现。这会抛出另一个 Reference Error:

  • V8:x is not defined
  • Nitro:Cannot access uninitialized variable.
  • SpiderMonkey:可以访问,值是 undefined

注意 Nitro 的提示和第一行的不同。

第四行:

let x;

对 "x" 这个 Identifier(而不是对 x 这个 Reference 的 value)的重复声明(declare)会在 DeclareAndInitializeVariables 抛出 Syntax Error:

  • V8:Identifier 'x' has already been declared
  • Nitro:Can't create duplicate variable: 'x'
  • SpiderMonkey:redeclaration of let x

ES2015 在语法层面上通过各种约定尽量避免没有初始化的情况:const 必须要通过 assigment 表达式显式初始化、let 明确默认以 undefined 初始化、两种变量声明都不提升、提供了 block 语法。都是在修补 var 以及模糊的作用域带来的问题。生产环境中,已经非常少踩到变量声明相关的坑了…

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated.
A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer's AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created.

总之,为了实现这两条规范暗指的 TDZ 效果,V8 还做了很多工作。但总的来说,减少暧昧的不确定行为以及阻止这样的不确定行为成为一些无聊的题目,是好语言的价值之一。