เหตุใดตัวอย่างของ 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 = 1และb.e = 1ดำเนินการก่อน (ให้สิทธิ์การเชื่อมโยงบน=) จากนั้นa.x.y.z = ...ดำเนินการและล้มเหลว เหตุใดการbมอบหมายจึงส่งผ่านในกรณีหนึ่ง แต่ไม่ผ่านอีกกรณีหนึ่ง
Amadan

3
@NinaScholz เรายอมรับว่าyไม่มีทรัพย์สินอยู่บนa.x; แต่นั่นเป็นเรื่องจริงในทั้งสองกรณี เหตุใดจึงป้องกันการกำหนดด้านขวามือในกรณีที่สอง แต่ไม่ใช่ครั้งแรก ลำดับการดำเนินการต่างกันอย่างไร? (ฉันพูดถึงข้อผิดพลาดทางไวยากรณ์เนื่องจากเวลาของข้อผิดพลาดทางไวยากรณ์แตกต่างจากข้อผิดพลาดรันไทม์มาก)
Amadan

@Amadan หลังจากรันโค้ดคุณจะได้รับข้อผิดพลาดและใช้กว่าพิมพ์ชื่อตัวแปรอีกครั้งเพื่อดูค่า
Code Maniac

2
พบสิ่งนี้อธิบายวิธีการที่ Javascript ดำเนินการมอบหมายการดำเนินการecma-international.org/ecma-262/5.1/#sec-11.13
Solomon Tam

2
เป็นเรื่องที่น่าสนใจจากมุมมองทางทฤษฎี แต่สิ่งนี้อยู่ภายใต้หมวดหมู่ "นี่คือเหตุผลที่คุณไม่เขียนโค้ดแบบนั้น" ของพฤติกรรมที่ไม่คาดคิด
John Montgomery

คำตอบ:


152

อันที่จริงหากคุณอ่านข้อความแสดงข้อผิดพลาดอย่างถูกต้องกรณีที่ 1 และกรณีที่ 2 จะแสดงข้อผิดพลาดที่แตกต่างกัน

กรณีa.x.y:

ไม่สามารถตั้งค่าคุณสมบัติ 'y' ของ undefined

กรณีa.x.y.z:

ไม่สามารถอ่านคุณสมบัติ 'y' ของ 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)
}

ในความคิดเห็นโซโลมอน 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. GetValue(rref)อนุญาตเป็น rval

  4. โยนข้อยกเว้น SyntaxError ถ้า ... (ไม่เกี่ยวข้อง)

  5. โทรPutValue(lref, rval).

PutValueคือสิ่งที่พ่นTypeError:

  1. ToObject(base)ให้เป็น O

  2. หากผลลัพธ์ของการเรียก[[CanPut]]เมธอดภายในของ O ด้วยอาร์กิวเมนต์ P เป็นเท็จดังนั้น

    ก. ถ้า Throw เป็นจริงให้โยนข้อยกเว้น TypeError

ไม่มีอะไรที่สามารถกำหนดให้กับทรัพย์สินของundefined- The [[CanPut]]วิธีการภายในของมักจะกลับมาundefinedfalse

กล่าวอีกนัยหนึ่ง: ล่ามจะแยกวิเคราะห์ทางด้านซ้ายจากนั้นแยกวิเคราะห์ทางด้านขวาจากนั้นจะแสดงข้อผิดพลาดหากไม่สามารถกำหนดคุณสมบัติทางด้านซ้ายมือให้ได้

เมื่อคุณทำ

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เหมือนข้างบน แต่การเข้าถึง a.x.yโยนข้อผิดพลาดเพราะคุณสมบัติที่ไม่สามารถเข้าถึงได้บนyundefined


20
เคล็ดลับตัวดำเนินการลูกน้ำที่ดี - ไม่เคยคิดที่จะใช้วิธีนี้ (สำหรับการดีบักเท่านั้นแน่นอน)!
ecraig12345

2
s / parse /
eval

3

พิจารณารหัสต่อไปนี้:

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

โครงร่างคร่าวๆของขั้นตอนที่จำเป็นในการรันโค้ดมีดังต่อไปนี้ref :

  1. ประเมินด้านซ้ายมือ. สองสิ่งที่ควรทราบ:
    • การประเมินนิพจน์ไม่เหมือนกับการรับค่าของนิพจน์
    • การประเมินสถานที่ให้บริการการเข้าถึงโทษเช่นa.x.yผลตอบแทนอ้างอิงเตะประกอบด้วยค่าฐานa.x(undefined) และชื่ออ้างอิง ( y)
  2. ประเมินด้านขวามือ.
  3. รับมูลค่าของผลลัพธ์ที่ได้รับในขั้นตอนที่ 2
  4. ตั้งค่าของการอ้างอิงที่ได้รับในขั้นตอนที่ 1 เป็นค่าที่ได้รับในขั้นตอนที่ 3 เช่นตั้งค่าคุณสมบัติyของไม่ได้กำหนดเป็นค่า นี้ควรจะโยน TypeError ยกเว้นโทษ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.