คำอธิบายสำหรับพฤติกรรม JavaScript ที่แปลกประหลาดเหล่านี้ที่กล่าวถึงในการพูดคุย 'Wat' สำหรับ CodeMash 2012 คืออะไร?


753

การพูดคุย 'Wat' สำหรับ CodeMash 2012โดยทั่วไปชี้ให้เห็นถึงความแปลกประหลาดของทับทิมและจาวาสคริปต์

ผมได้ทำ JSFiddle ของผลที่http://jsfiddle.net/fe479/9/

พฤติกรรมที่เฉพาะเจาะจงกับ JavaScript (ตามที่ฉันไม่รู้ว่า Ruby) มีการระบุไว้ด้านล่าง

ฉันพบใน JSFiddle ว่าผลลัพธ์บางอย่างของฉันไม่ตรงกับผลลัพธ์ในวิดีโอและฉันไม่แน่ใจว่าทำไม อย่างไรก็ตามฉันอยากรู้ว่า JavaScript จัดการกับเบื้องหลังในแต่ละกรณีได้อย่างไร

Empty Array + Empty Array
[] + []
result:
<Empty String>

ฉันค่อนข้างสงสัยเกี่ยวกับ+โอเปอเรเตอร์เมื่อใช้กับอาร์เรย์ใน JavaScript ตรงกับผลลัพธ์ของวิดีโอ

Empty Array + Object
[] + {}
result:
[Object]

ตรงกับผลลัพธ์ของวิดีโอ เกิดอะไรขึ้นที่นี่? ทำไมถึงเป็นวัตถุ อะไร+ประกอบการทำอย่างไร

Object + Empty Array
{} + []
result:
[Object]

สิ่งนี้ไม่ตรงกับวิดีโอ วิดีโอแนะนำว่าผลลัพธ์คือ 0 ในขณะที่ฉันได้รับ [Object]

Object + Object
{} + {}
result:
[Object][Object]

สิ่งนี้ไม่ตรงกับวิดีโอและวิธีการส่งออกตัวแปรส่งผลให้วัตถุสองรายการ บางที JSFiddle ของฉันผิด

Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

กำลังทำ +1 ผลลัพธ์ในwat1wat1wat1wat1...

ฉันสงสัยว่านี่เป็นเพียงพฤติกรรมที่ตรงไปตรงมาที่พยายามลบจำนวนจากผลลัพธ์สตริงใน NaN


4
{} + [] เป็นพื้นฐานที่ยุ่งยากและขึ้นอยู่กับการนำไปใช้อย่างที่ฉันอธิบายที่นี่เพราะมันขึ้นอยู่กับการแยกวิเคราะห์เป็นคำสั่งหรือการแสดงออก คุณกำลังทดสอบสภาพแวดล้อมแบบใด (ฉันได้รับ 0 ที่คาดหวังใน Firefow และ Chrome แต่ได้รับ "[object Object]" ใน NodeJs)
hugomg

1
ฉันใช้ Firefox 9.0.1 บน windows 7 และ JSFiddle จะประเมินเป็น [Object]
NibblyPig

@missingno ฉันได้รับ 0 ใน NodeJS REPL
OrangeDog

41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson

1
@missingno โพสต์คำถามที่นี่{} + {}แต่สำหรับ
IonicăBizău

คำตอบ:


1479

นี่คือรายการคำอธิบายของผลลัพธ์ที่คุณเห็น (และควรจะเห็น) ลำดับที่ฉันใช้มาจากมาตรฐาน ECMA-262

  1. [] + []

    เมื่อใช้โอเปอเรเตอร์การเพิ่มทั้งตัวถูกดำเนินการทางซ้ายและขวาจะถูกแปลงเป็นไพรเมอร์ก่อน ( §11.6.1 ) ตาม§9.1การแปลงออบเจ็กต์ (ในกรณีนี้คืออาเรย์) ไปยังแบบดั้งเดิมจะส่งกลับค่าเริ่มต้นซึ่งสำหรับวัตถุที่มีtoString()วิธีการที่ถูกต้องคือผลลัพธ์ของการโทรobject.toString()( §8.12.8 ) สำหรับอาร์เรย์นี้จะเหมือนกับการโทรarray.join()( §15.4.4.2 ) การเข้าร่วมอาเรย์ที่ว่างเปล่าส่งผลให้เกิดสตริงว่างดังนั้นขั้นตอนที่ # 7 ของโอเปอเรเตอร์การเติมจะส่งคืนการต่อข้อมูลของสองสตริงว่างซึ่งเป็นสตริงว่าง

  2. [] + {}

    ในทำนองเดียวกัน[] + []ตัวถูกดำเนินการทั้งสองจะถูกแปลงเป็นแบบดั้งเดิมก่อน สำหรับ "ออบเจกต์วัตถุ" (§15.2) นี่คือผลลัพธ์ของการโทรอีกครั้งobject.toString()ซึ่งสำหรับวัตถุที่ไม่เป็นโมฆะและไม่ได้กำหนดไว้คือ"[object Object]"( is15.2.4.2 )

  3. {} + []

    {}นี่ไม่ได้แยกวิเคราะห์เป็นวัตถุ แต่แทนที่จะเป็นบล็อกที่ว่างเปล่า ( §12.1อย่างน้อยตราบเท่าที่คุณไม่ได้บังคับให้คำสั่งที่จะต้องมีการแสดงออก แต่เกี่ยวกับว่าภายหลัง) +[]ค่าตอบแทนของบล็อกที่ว่างเปล่าว่างเปล่าดังนั้นผลของคำสั่งที่เป็นเช่นเดียวกับ เอก+ผู้ประกอบการ ( §11.4.6 ) ToNumber(ToPrimitive(operand))ผลตอบแทน ในฐานะที่เรารู้อยู่แล้วว่าToPrimitive([])เป็นสตริงที่ว่างเปล่าและเป็นไปตาม§9.3.1 , ToNumber("")คือ 0

  4. {} + {}

    คล้ายกับกรณีก่อนหน้าตัวแรก{}ถูกแยกวิเคราะห์เป็นบล็อกที่มีค่าส่งคืนที่ว่างเปล่า อีกครั้ง+{}เป็นเช่นเดียวกับToNumber(ToPrimitive({}))และToPrimitive({})เป็น"[object Object]"(ดู[] + {}) ดังนั้นเพื่อให้ได้ผลมาจากการ+{}ที่เราต้องใช้ในสตริงToNumber "[object Object]"เมื่อทำตามขั้นตอนตั้งแต่ . 39.3.1เราจะได้NaNผลลัพธ์ดังนี้:

    หากไวยากรณ์ไม่สามารถแปลสตริงการขยายตัวของStringNumericLiteralแล้วผลของToNumberเป็นน่าน

  5. Array(16).join("wat" - 1)

    เป็นต่อ§15.4.1.1และ§15.4.2.2 , Array(16)สร้างแถวใหม่ที่มีความยาว 16 จะได้รับความคุ้มค่าของการโต้แย้งที่จะเข้าร่วมการ§11.6.2ขั้นตอนที่ 5 และที่ 6 แสดงให้เห็นว่าเรามีการแปลงตัวถูกดำเนินการทั้งสองไป ToNumberจำนวนการใช้ ToNumber(1)เป็นเพียง 1 ( §9.3 ) ในขณะที่ToNumber("wat")อีกครั้งNaNตาม§9.3.1 ทำตามขั้นตอนที่ 7 ของ§11.6.2 , §11.6.3กำหนดว่า

    หากทั้งสองตัวถูกดำเนินการคือน่านผลที่ได้คือน่าน

    ดังนั้นการโต้แย้งที่จะมีArray(16).join NaNต่อไปนี้§15.4.4.5 ( Array.prototype.join) เราต้องเรียกToStringอาร์กิวเมนต์ซึ่งคือ"NaN"( §9.8.1 ):

    หากเมตรเป็นน่าน"NaN"กลับสตริง

    ทำตามขั้นตอนที่ 10 ของ§15.4.4.5เราได้รับการทำซ้ำ 15 ครั้งของการต่อข้อมูล"NaN"และสตริงว่างซึ่งเท่ากับผลลัพธ์ที่คุณเห็น เมื่อใช้"wat" + 1แทน"wat" - 1เป็นอาร์กิวเมนต์ที่แปลงผู้ประกอบการนอกจากนี้1ยังสตริงแทนของการแปลงไปยังหมายเลขจึงโทรได้อย่างมีประสิทธิภาพ"wat"Array(16).join("wat1")

สำหรับสาเหตุที่คุณเห็นผลลัพธ์ที่แตกต่างกันสำหรับ{} + []กรณี: เมื่อใช้เป็นอาร์กิวเมนต์ฟังก์ชันคุณบังคับให้คำสั่งเป็นExpressionStatementซึ่งทำให้ไม่สามารถแยกวิเคราะห์{}เป็นบล็อกว่างได้ดังนั้นจึงแยกวิเคราะห์เป็นวัตถุว่างเปล่าแทน ตามตัวอักษร


2
เหตุใด [] +1 => "1" และ [] -1 => -1
Rob Elsner

4
@RobElsner []+1สวยมากตามตรรกะเดียวกันเช่น[]+[]เดียวกับ1.toString()Rhs operand เพื่อ[]-1ดูคำอธิบายของ"wat"-1จุดที่ 5 โปรดจำไว้ว่าToNumber(ToPrimitive([]))เป็น 0 (จุด 3)
Ventero

4
คำอธิบายนี้หายไป / ละเว้นรายละเอียดมาก เช่น "การแปลงออบเจ็กต์ (ในกรณีนี้คืออาเรย์) ไปยังค่าดั้งเดิมจะส่งกลับค่าเริ่มต้นซึ่งสำหรับวัตถุที่มีเมธอด toString () ที่ถูกต้องนั้นเป็นผลมาจากการเรียกใช้ object.toString ()" โดยสิ้นเชิง เรียกใช้ก่อน แต่เนื่องจากค่าส่งคืนไม่ใช่ดั้งเดิม (เป็นอาร์เรย์) จึงใช้ toString ของ [] แทน ฉันอยากจะแนะนำให้ดูสิ่งนี้แทนคำอธิบายที่ลึกจริง2ality.com/2012/01/object-plus-object.html
jahav

30

นี่เป็นความคิดเห็นมากกว่าคำตอบ แต่ด้วยเหตุผลบางอย่างฉันไม่สามารถแสดงความคิดเห็นในคำถามของคุณ ฉันต้องการแก้ไขรหัส JSFiddle ของคุณ อย่างไรก็ตามฉันโพสต์สิ่งนี้บน Hacker News และมีคนแนะนำว่าฉันโพสต์ไว้ที่นี่อีกครั้ง

ปัญหาในรหัส JSFiddle คือ({})(การเปิดวงเล็บด้านในของวงเล็บ) ไม่เหมือนกับ{}(เปิดวงเล็บปีกกาเป็นจุดเริ่มต้นของบรรทัดของรหัส) ดังนั้นเมื่อคุณพิมพ์out({} + [])คุณกำลังบังคับให้เป็นอะไรบางอย่างที่มันไม่ได้เมื่อคุณพิมพ์{} {} + []นี่เป็นส่วนหนึ่งของ 'wat'-ness ของ Javascript โดยรวม

แนวคิดพื้นฐานคือ JavaScript ที่เรียบง่ายต้องการให้ทั้งสองรูปแบบเหล่านี้:

if (u)
    v;

if (x) {
    y;
    z;
}

ต้องการทำเช่นนั้นทั้งสองการตีความที่ทำจากรั้งเปิด: 1. มันจะไม่จำเป็นต้องใช้และ 2. มันสามารถปรากฏที่ใดก็ได้

นี่เป็นการเคลื่อนไหวที่ผิด รหัสจริงไม่มีวงเล็บปีกกาเปิดปรากฏขึ้นตรงกลางและรหัสจริงนั้นมีแนวโน้มที่จะบอบบางกว่าเมื่อใช้รูปแบบแรกแทนที่จะเป็นรูปที่สอง (ประมาณหนึ่งครั้งทุก ๆ เดือนที่งานสุดท้ายของฉันฉันจะถูกเรียกไปที่โต๊ะทำงานของเพื่อนร่วมงานเมื่อการดัดแปลงรหัสของฉันไม่ทำงานและปัญหาคือพวกเขาต้องการเพิ่มบรรทัดลงใน "ถ้า" โดยไม่เพิ่มลอน เครื่องมือจัดฟันในที่สุดฉันก็เพิ่งใช้นิสัยที่ต้องใช้เครื่องมือจัดฟันแบบหยิกเสมอแม้ว่าคุณจะเขียนแค่บรรทัดเดียว)

โชคดีที่หลาย ๆ กรณี eval () จะทำซ้ำจาวาสคริปต์ทั้งหมด รหัส JSFiddle ควรอ่าน:

function out(code) {
    function format(x) {
        return typeof x === "string" ?
            JSON.stringify(x) : x;
    }   
    document.writeln('&gt;&gt;&gt; ' + code);
    document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");

[และนี่เป็นครั้งแรกที่ฉันได้เขียน document.writeln ในหลายปีที่ผ่านมาและฉันรู้สึกสกปรกเล็กน้อยที่จะเขียนอะไรเกี่ยวกับทั้ง document.writeln () และ eval ()]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- ผมไม่เห็นด้วย (ประเภท): ฉันมีมักจะอยู่ในบล็อกมือสองที่ผ่านมาเช่นนี้กับตัวแปรขอบเขตใน C นิสัยนี้ถูกหยิบขึ้นมาอีกสักพักเมื่อทำ C แบบฝังซึ่งตัวแปรในสแต็กใช้พื้นที่ดังนั้นถ้าพวกเขาไม่ต้องการอีกต่อไปเราต้องการให้มีพื้นที่ว่างในตอนท้ายของบล็อก อย่างไรก็ตาม ECMAScript จะ จำกัด ขอบเขตภายในฟังก์ชัน () {} บล็อกเท่านั้น ดังนั้นในขณะที่ฉันไม่เห็นด้วยกับแนวคิดที่ผิดฉันยอมรับว่าการดำเนินการใน JS ผิดพลาด( อาจ )
Jess Telford

4
@JessTelford ใน ES6 คุณสามารถใช้letเพื่อประกาศตัวแปรที่กำหนดขอบเขตบล็อก
Oriol

19

ฉันที่สอง @ ทางออกของ Ventero หากคุณต้องการคุณสามารถดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการ+แปลงตัวถูกดำเนินการ

ขั้นตอนแรก (§9.1):แปลงทั้งสองตัวถูกดำเนินการ primitives (ค่าดั้งเดิมมีundefined, null, บูลี, ตัวเลข, สตริง; ค่าอื่น ๆ ทั้งหมดเป็นวัตถุรวมทั้งอาร์เรย์และฟังก์ชั่น) หากตัวถูกดำเนินการดั้งเดิมแล้วคุณจะทำ ถ้าไม่ใช่มันเป็นวัตถุobjและดำเนินการตามขั้นตอนต่อไปนี้:

  1. โทรobj.valueOf(). ถ้ามันคืนค่าดั้งเดิมคุณก็เสร็จแล้ว อินสแตนซ์โดยตรงของObjectและอาร์เรย์ส่งคืนตัวเองดังนั้นคุณยังไม่ได้ทำ
  2. โทรobj.toString(). ถ้ามันคืนค่าดั้งเดิมคุณก็เสร็จแล้ว {}และ[]ทั้งสองส่งคืนสตริงดังนั้นคุณเสร็จแล้ว
  3. TypeErrorมิฉะนั้นโยน

สำหรับวันที่จะเปลี่ยนขั้นตอนที่ 1 และ 2 คุณสามารถสังเกตพฤติกรรมการแปลงดังนี้:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

การโต้ตอบ ( Number()แปลงเป็นแบบดั้งเดิมแล้วเป็นจำนวน):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

ขั้นตอนที่สอง (§11.6.1):หากตัวถูกดำเนินการตัวใดตัวหนึ่งเป็นสตริงตัวถูกดำเนินการตัวอื่นจะถูกแปลงเป็นสตริงและผลลัพธ์ถูกสร้างขึ้นโดยการต่อสองสายเข้าด้วยกัน มิฉะนั้นตัวถูกดำเนินการทั้งสองจะถูกแปลงเป็นตัวเลขและผลลัพธ์ถูกสร้างขึ้นโดยการเพิ่มพวกมัน

คำอธิบายโดยละเอียดเพิ่มเติมเกี่ยวกับกระบวนการแปลง:“ {} + {} ใน JavaScript คืออะไร


13

เราอาจอ้างถึงข้อกำหนดและเป็นสิ่งที่ยอดเยี่ยมและถูกต้องที่สุด แต่กรณีส่วนใหญ่สามารถอธิบายได้ด้วยวิธีที่เข้าใจได้ง่ายขึ้นด้วยข้อความต่อไปนี้:

  • +และ-โอเปอเรเตอร์ทำงานเฉพาะกับค่าดั้งเดิม โดยเฉพาะอย่างยิ่ง+(เพิ่มเติม) ทำงานกับสตริงหรือตัวเลขและ+(unary) และ-(การลบและ unary) ใช้ได้กับตัวเลขเท่านั้น
  • ฟังก์ชันหรือตัวดำเนินการดั้งเดิมทั้งหมดที่คาดว่าค่าดั้งเดิมเป็นอาร์กิวเมนต์จะแปลงอาร์กิวเมนต์นั้นเป็นชนิดดั้งเดิมที่ต้องการ มันทำด้วยvalueOfหรือtoStringที่มีอยู่ในวัตถุใด ๆ นั่นเป็นเหตุผลที่ฟังก์ชั่นหรือตัวดำเนินการดังกล่าวไม่ได้เกิดข้อผิดพลาดเมื่อเรียกใช้บนวัตถุ

ดังนั้นเราอาจพูดได้ว่า:

  • [] + []เป็นเช่นเดียวกับที่เป็นเช่นเดียวกับString([]) + String([]) '' + ''ฉันกล่าวถึงข้างต้นว่า+(นอกจากนี้) ยังใช้ได้กับตัวเลข แต่ไม่มีการแสดงตัวเลขที่ถูกต้องของอาร์เรย์ใน JavaScript ดังนั้นการเพิ่มสตริงจะใช้แทน
  • [] + {}เหมือนกันกับString([]) + String({})ที่เป็น'' + '[object Object]'
  • {} + []. อันนี้สมควรอธิบายเพิ่มเติม (ดูคำตอบ Ventero) ในกรณีที่วงเล็บปีกกาได้รับการปฏิบัติไม่เป็นวัตถุ +[]แต่เป็นบล็อกที่ว่างเปล่าดังนั้นมันจะออกมาเป็นเช่นเดียวกับ เอกทำงานเฉพาะกับตัวเลขเพื่อให้การดำเนินงานพยายามที่จะได้รับหมายเลขออกจาก+ []ครั้งแรกมันจะพยายามvalueOfซึ่งในกรณีของอาร์เรย์ส่งคืนวัตถุเดียวกันดังนั้นจึงลองวิธีสุดท้าย: การแปลงtoStringผลลัพธ์ให้เป็นตัวเลข เราอาจจะเขียนเป็น+Number(String([]))ซึ่งเป็นเช่นเดียวกับที่เป็นเช่นเดียวกับ+Number('')+0
  • Array(16).join("wat" - 1)การลบใช้-งานได้กับตัวเลขเท่านั้นดังนั้นจึงเป็นเช่นเดียวกับ: Array(16).join(Number("wat") - 1)เนื่องจาก"wat"ไม่สามารถแปลงเป็นตัวเลขที่ถูกต้องได้ เราได้รับNaNและดำเนินการทางคณิตศาสตร์ใด ๆ เกี่ยวกับNaNผลลัพธ์ที่มีเพื่อให้เรามี:NaNArray(16).join(NaN)

0

เพื่อค้ำจุนสิ่งที่แบ่งปันไว้ก่อนหน้านี้

สาเหตุพื้นฐานของพฤติกรรมนี้ส่วนหนึ่งเป็นเพราะลักษณะที่อ่อนแอของ JavaScript ตัวอย่างเช่นนิพจน์ 1 +“ 2” มีความกำกวมเนื่องจากมีการตีความสองแบบที่เป็นไปได้ตามชนิดของตัวถูกดำเนินการ (int, สตริง) และ (int int):

  • ผู้ใช้ตั้งใจที่จะต่อเชื่อมสตริงสองตัวผลลัพธ์:“ 12”
  • ผู้ใช้ตั้งใจที่จะเพิ่มตัวเลขสองตัวผลลัพธ์: 3

ดังนั้นด้วยประเภทอินพุตที่แตกต่างกัน

อัลกอริธึมเพิ่มเติม

  1. ตัวถูกบังคับให้ถูกบังคับให้เป็นค่าดั้งเดิม

จาวาสคริปต์เบื้องต้นคือสตริง, จำนวน, โมฆะ, ไม่ได้กำหนดและบูลีน (สัญลักษณ์กำลังจะมาใน ES6) ค่าอื่นใดคือวัตถุ (เช่นอาร์เรย์ฟังก์ชันและวัตถุ) กระบวนการบีบบังคับสำหรับการแปลงวัตถุเป็นค่าดั้งเดิมอธิบายไว้ดังนี้:

  • หากส่งคืนค่าดั้งเดิมเมื่อ object.valueOf () ถูกเรียกใช้ให้ส่งคืนค่านี้มิฉะนั้นจะดำเนินการต่อ

  • หากส่งคืนค่าดั้งเดิมเมื่อ object.toString () ถูกเรียกใช้ให้ส่งคืนค่านี้มิฉะนั้นจะดำเนินการต่อ

  • โยน TypeError

หมายเหตุ: สำหรับค่าวันที่คำสั่งนั้นจะเรียกใช้ toString ก่อน valueOf

  1. หากค่าตัวถูกดำเนินการใด ๆ เป็นสตริงให้ทำการต่อสตริงเข้าด้วยกัน

  2. มิฉะนั้นแปลงตัวถูกดำเนินการทั้งสองเป็นค่าตัวเลขแล้วเพิ่มค่าเหล่านี้

การรู้ค่าบังคับชนิดต่าง ๆ ใน JavaScript จะช่วยทำให้ผลลัพธ์ที่สับสนชัดเจนขึ้น ดูตารางการบีบบังคับด้านล่าง

+-----------------+-------------------+---------------+
| Primitive Value |   String value    | Numeric value |
+-----------------+-------------------+---------------+
| null            | null            | 0             |
| undefined       | undefined       | NaN           |
| true            | true            | 1             |
| false           | false           | 0             |
| 123             | 123             | 123           |
| []              | “”                | 0             |
| {}              | “[object Object]” | NaN           |
+-----------------+-------------------+---------------+

นอกจากนี้ยังเป็นการดีที่จะรู้ว่าตัวดำเนินการ + ของ JavaScript นั้นมีความสัมพันธ์ด้านซ้ายเนื่องจากจะเป็นตัวกำหนดว่าผลลัพธ์จะเป็นกรณีที่เกี่ยวข้องกับการดำเนินการมากกว่าหนึ่ง +

การใช้ประโยชน์จากดังนั้น 1 + "2" จะให้ "12" เพราะการเพิ่มใด ๆ ที่เกี่ยวข้องกับสตริงจะเริ่มต้นด้วยการต่อสตริง

คุณสามารถอ่านตัวอย่างเพิ่มเติมในโพสต์บล็อกนี้ (ข้อจำกัดความรับผิดชอบฉันเขียนไว้)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.