开发者问题收集

为什么这些 JavaScript 片段即使都遇到错误,但行为却不同?

2019-02-12
6434
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
3个回答

实际上,如果您正确阅读错误消息,案例 1 和案例 2 会引发不同的错误。

案例 a.x.y :

Cannot set property 'y' of undefined

案例 a.x.y.z :

Cannot read property 'y' of undefined

我想最好用简单的英语一步一步地执行来描述它。

案例 1

// 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)
}

案例 2

// 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 发现 有关赋值操作的 ECMA 文档

yqlim
2019-02-12

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

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

查看 规范

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]] 内部方法将始终返回 false

换句话说:解释器先解析左侧,再解析右侧, 然后 如果左侧的属性无法赋值,则会引发错误。

当您执行

a.x.y = b.e = 1

直到调用 PutValue 之前,左侧都已 成功解析 ;直到解析右侧之后,才会考虑 .x 属性的计算结果为 undefined 这一事实。解释器将其视为“将某个值分配给 undefined 的属性“y”,而分配给 undefined 的属性只会在 PutValue 中抛出异常。

相反:

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

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

CertainPerformance
2019-02-12

考虑以下代码:

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

执行代码所需步骤的粗略概述如下 ref

  1. 评估左侧。需要记住两件事:
    • 评估表达式与获取表达式的值不同。
    • 评估属性访问器 ref 例如 a.x.y 返回一个引用 ref ,由基值 a.x (未定义)和引用名称( y )组成。
  2. 评估右侧。
  3. 获取步骤 2 中获得的结果的值。
  4. 将步骤 1 中获得的引用的值设置为步骤 3 中获得的值,即将未定义的属性 y 设置为该值。这应该会抛出一个 TypeError 异常 ref
Salman Arshad
2019-02-13