为什么 JavaScript 的这些片段虽然都遇到错误但表现却不同?

Why do these snippets of JavaScript behave differently even though they both encounter an error?

var a = {}
var b = {}

try{
  a.x.y = b.e = 1 // Uncaught TypeError: Cannot set property 'y' of undefined
} catch(err) {
  console.error(err);
}
console.log(b.e) // 1

var a = {}
var b = {}

try {
  a.x.y.z = b.e = 1 // Uncaught TypeError: Cannot read property 'y' of undefined
} catch(err) {
  console.error(err);
}

console.log(b.e) // undefined

实际上,如果您正确阅读错误消息,情况 1 和情况 2 会抛出不同的错误。

案例a.x.y

Cannot set property 'y' of undefined

案例a.x.y.z:

Cannot read property 'y' of undefined

我想最好用通俗易懂的逐步执行来描述它。

案例一

// 1. Declare variable `a`
// 2. Define variable `a` as {}
var a = {}

// 1. Declare variable `b`
// 2. Define variable `b` as {}
var b = {}

try {

  /**
   *  1. Read `a`, gets {}
   *  2. Read `a.x`, gets undefined
   *  3. Read `b`, gets {}
   *  4. Set `b.z` to 1, returns 1
   *  5. Set `a.x.y` to return value of `b.z = 1`
   *  6. Throws "Cannot **set** property 'y' of undefined"
   */
  a.x.y = b.z = 1
  
} catch(e){
  console.error(e.message)
} finally {
  console.log(b.z)
}

案例二

// 1. Declare variable `a`
// 2. Define variable `a` as {}
var a = {}

// 1. Declare variable `b`
// 2. Define variable `b` as {}
var b = {}

try {

  /**
   *  1. Read `a`, gets {}
   *  2. Read `a.x`, gets undefined
   *  3. Read `a.x.y`, throws "Cannot **read** property 'y' of undefined".
   */
  a.x.y.z = b.z = 1
  
} catch(e){
  console.error(e.message)
} finally {
  console.log(b.z)
}

在评论中,Solomon Tam found this ECMA documentation about assignment operation

当您利用括号内的逗号运算符来查看执行了哪些部分时,操作顺序会更清晰:

var a = {}
var b = {}

try{
 // Uncaught TypeError: Cannot set property 'y' of undefined
  a
    [console.log('x'), 'x']
    [console.log('y'), 'y']
    = (console.log('right hand side'), b.e = 1);
} catch(err) {
  console.error(err);
}
console.log(b.e) // 1

var a = {}
var b = {}

try {
  // Uncaught TypeError: Cannot read property 'y' of undefined
  a
    [console.log('x'), 'x']
    [console.log('y'), 'y']
    [console.log('z'), 'z']
    = (console.log('right hand side'), b.e = 1);
} catch(err) {
  console.error(err);
}

console.log(b.e) // undefined

查看 spec:

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Let lref be the result of evaluating LeftHandSideExpression.

  2. Let rref be the result of evaluating AssignmentExpression.

  3. Let rval be GetValue(rref).

  4. Throw a SyntaxError exception if... (irrelevant)

  5. Call PutValue(lref, rval).

PutValue 是抛出 TypeError:

  1. Let O be ToObject(base).

  2. If the result of calling the [[CanPut]] internal method of O with argument P is false, then

    a. If Throw is true, then throw a TypeError exception.

无法将任何内容分配给 undefined 的 属性 - undefined[[CanPut]] 内部方法将始终 return false

换句话说:解释器解析左边,然后解析右边,然后如果左边的属性抛出错误-手边不能分配给。

当你这样做时

a.x.y = b.e = 1

左侧 已成功解析 直到 PutValue 被调用; .x 属性 的计算结果为 undefined 这一事实直到右侧被解析后才被考虑。解释器将其视为“为未定义的 属性 “y” 分配一些值”,并且分配给 undefined 的 属性 只会抛出内部 PutValue.

对比:

a.x.y.z = b.e = 1

解释器永远不会达到它试图分配给 z 属性 的地步,因为它首先必须将 a.x.y 解析为一个值。如果 a.x.y 解析为一个值(甚至解析为 undefined),那就没问题了 - 会像上面那样在 PutValue 中抛出错误。但是 accessing a.x.y 抛出一个错误,因为 属性 y 无法在 undefined.

上访问

考虑以下代码:

var a = {};
a.x.y = console.log("evaluating right hand side"), 1;

执行代码所需的大致步骤如下ref:

  1. 评估左侧。要记住两件事:
    • 计算表达式与获取表达式的值不同。
    • 正在计算一个 属性 访问器 ref e.g. a.x.y returns a reference ref,由基值 a.x(未定义)和引用名称(y)组成.
  2. 评估右侧。
  3. 获取第2步得到的result的值
  4. 将第1步得到的reference的值设置为第3步得到的值即设置undefined的属性y为该值。这应该抛出 TypeError 异常 ref.