为什么这两个JavaScript代码片段都遇到了错误,但行为却不同?

107

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
@NinaScholz说:“我不理解。没有语法错误;因此我会假设b.z=1b.e=1首先执行(考虑到=的右结合性),然后a.x.y.z = ...执行并失败;为什么在一个情况下'b'赋值通过而在另一个情况下不通过?” - Amadan
3
我们认同a.x上不存在属性y,但这在两种情况下都是正确的。为什么它会阻止第二种情况下的右侧赋值而不是第一种情况?执行顺序有何不同之处?(我提到语法错误是因为语法错误的时间点与运行时错误非常不同。) - Amadan
@Amadan在运行代码后会收到错误提示,然后输入变量名以再次查看其值。 - Code Maniac
3
找到了这篇文章,描述了JavaScript如何进行赋值操作。https://www.ecma-international.org/ecma-262/5.1/#sec-11.13 - Solomon Tam
你认为它们的区别是什么?在提问之前,你有尝试推理吗?你的研究结果如何?换句话说,在提问之前,你是否做了功课? - jpmc26
2
从理论角度来看很有趣,但这绝对属于“这就是为什么你不要写那样的代码”的意外行为类别。 - John Montgomery
3个回答

151

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

对于Case a.x.y

无法设置未定义属性 'y'

对于Case a.x.y.z

无法读取未定义属性 'y'

我认为最好通过逐步执行易懂的英语来描述它。

Case 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 文档


57

当您利用方括号表示法中的逗号运算符查看执行哪些部分时,操作顺序会更清晰:

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

查看规范

生产AssignmentExpression : LeftHandSideExpression = AssignmentExpression按以下方式计算:

  1. 让lref成为评估LeftHandSideExpression的结果。

  2. 让rref成为评估AssignmentExpression的结果。

  3. 让rval为GetValue(rref)

  4. 如果...抛出SyntaxError异常(不相关)

  5. 调用PutValue(lref, rval)

PutValue会引发TypeError

  1. 让O为ToObject(base)

  2. 如果使用参数P调用O的 [[CanPut]] 内部方法的结果为false,则

    a. 如果 Throw 为true,则抛出 TypeError 异常。

无法对undefined的属性赋值 - undefined[[CanPut]]内部方法将始终返回false

换句话说:解释器首先解析左侧,然后解析右侧,然后如果不能将左侧的属性赋值,则抛出错误。

当你执行

a.x.y = b.e = 1

左侧被成功解析,直到调用PutValue;只有在解析右侧之后才考虑.x属性的值为undefined这一事实。解释器将其视为“将某个值分配给未定义变量的属性",而将值分配给undefined的属性仅在PutValue内部抛出异常。
相比之下:
a.x.y.z = b.e = 1

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


20
不错的逗号操作符技巧——从未想过以这种方式使用它(仅限于调试)! - ecraig12345
2
s/parse/evaluate/ - Bergi

3
请考虑以下代码:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接