Lời giải thích cho những hành vi kỳ quái này được đề cập trong bài nói chuyện 'Wat' cho CodeMash 2012 là gì?


753

Cuộc nói chuyện 'Wat' cho CodeMash 2012 về cơ bản chỉ ra một vài điều kỳ quái với Ruby và JavaScript.

Tôi đã tạo một JSFiddle về kết quả tại http://jsfiddle.net/fe479/9/ .

Các hành vi dành riêng cho JavaScript (như tôi không biết Ruby) được liệt kê bên dưới.

Tôi đã tìm thấy trong JSFiddle rằng một số kết quả của tôi không tương ứng với những kết quả trong video và tôi không chắc tại sao. Tuy nhiên, tôi tò mò muốn biết cách JavaScript xử lý hoạt động đằng sau hậu trường trong từng trường hợp.

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

Tôi khá tò mò về +toán tử khi được sử dụng với các mảng trong JavaScript. Điều này phù hợp với kết quả của video.

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

Điều này phù hợp với kết quả của video. Những gì đang xảy ra ở đây? Tại sao đây là một đối tượng. Người +điều hành làm gì?

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

Điều này không phù hợp với video. Video cho thấy kết quả là 0, trong khi tôi nhận được [Object].

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

Điều này cũng không khớp với video và làm thế nào để xuất ra một kết quả biến trong hai đối tượng? Có lẽ JSFiddle của tôi sai.

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

Làm wat + 1 kết quả trong wat1wat1wat1wat1...

Tôi nghi ngờ đây chỉ là hành vi đơn giản khi cố gắng trừ một số từ một chuỗi kết quả trong NaN.


4
{} + [] Về cơ bản là phụ thuộc duy nhất và khó thực hiện, như tôi giải thích ở đây , bởi vì nó phụ thuộc vào việc được phân tích cú pháp như một câu lệnh hoặc như một biểu thức. Bạn đang thử nghiệm môi trường nào (Tôi đã nhận được 0 mong đợi trong Firefow và Chrome nhưng có "[Object object]" trong NodeJs)?
hugomg

1
Tôi đang chạy Firefox 9.0.1 trên windows 7 và JSFiddle đánh giá nó thành [Object]
NibblyPig

@missingno Tôi nhận được 0 trong NodeJS REPL
OrangeDog

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

1
@missingno Đăng câu hỏi ở đây , nhưng cho {} + {}.
Ionică Bizău

Câu trả lời:


1479

Dưới đây là danh sách các giải thích cho kết quả bạn đang thấy (và được cho là đang nhìn thấy). Các tài liệu tham khảo tôi đang sử dụng là từ tiêu chuẩn ECMA-262 .

  1. [] + []

    Khi sử dụng toán tử cộng, cả hai toán hạng bên trái và bên phải được chuyển đổi thành nguyên hàm trước ( §11.6.1 ). Theo §9.1 , việc chuyển đổi một đối tượng (trong trường hợp này là một mảng) thành nguyên thủy trả về giá trị mặc định của nó, đối với các đối tượng có toString()phương thức hợp lệ là kết quả của việc gọi object.toString()( §8.12.8 ). Đối với mảng, điều này giống như cách gọi array.join()( §15.4.4.2 ). Việc nối một mảng trống dẫn đến một chuỗi trống, vì vậy bước # 7 của toán tử bổ sung trả về phép nối của hai chuỗi trống, đó là chuỗi rỗng.

  2. [] + {}

    Tương tự như vậy [] + [], cả hai toán hạng đều được chuyển đổi thành nguyên thủy trước. Đối với "Đối tượng đối tượng" (§15.2), đây lại là kết quả của việc gọi object.toString(), đối với các đối tượng không null, không xác định là"[object Object]" ( §15.2.4.2 ).

  3. {} + []

    {}đây không được phân tích cú pháp dưới dạng một đối tượng, mà thay vào đó là một khối trống ( §12.1 , ít nhất là miễn là bạn không buộc câu lệnh đó là một biểu thức, nhưng sẽ nói thêm về điều đó sau). Giá trị trả về của các khối trống là trống, do đó, kết quả của câu lệnh đó giống như +[]. +Toán tử đơn nguyên ( §11.4.6 ) trả về ToNumber(ToPrimitive(operand)). Như chúng ta đã biết, ToPrimitive([])là chuỗi rỗng, và theo §9.3.1 , ToNumber("")là 0.

  4. {} + {}

    Tương tự như trường hợp trước, lần đầu tiên {}được phân tích cú pháp dưới dạng một khối có giá trị trả về trống. Một lần nữa, +{}giống như ToNumber(ToPrimitive({})), và ToPrimitive({})"[object Object]"(xem [] + {}). Vì vậy, để có được kết quả +{}, chúng ta phải áp dụng ToNumbertrên chuỗi "[object Object]". Khi làm theo các bước từ §9.3.1 , chúng tôi nhận được NaNkết quả:

    Nếu ngữ pháp không thể hiểu Chuỗi là một bản mở rộng của StringNumericLiteral , thì kết quả của ToNumberNaN .

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

    Theo §15.4.1.1§15.4.2.2 , Array(16)tạo một mảng mới có độ dài 16. Để lấy giá trị của đối số để tham gia, §11.6.2 bước # 5 và # 6 cho thấy rằng chúng ta phải chuyển đổi cả hai toán hạng thành a số lượng sử dụng ToNumber. ToNumber(1)chỉ đơn giản là 1 ( §9.3 ), trong khi ToNumber("wat")một lần nữa là NaNtheo §9.3.1 . Theo bước 7 của §11.6.2 , §11.6.3 ra lệnh rằng

    Nếu một toán hạng là NaN , kết quả là NaN .

    Vì vậy, đối số Array(16).joinNaN. Theo §15.4.4.5 ( Array.prototype.join), chúng ta phải gọi ToStringđối số, đó là "NaN"( §9.8.1 ):

    Nếu mNaN , trả về Chuỗi "NaN".

    Theo bước 10 của §15.4.4.5 , chúng tôi nhận được 15 lần lặp lại của phép nối "NaN"và chuỗi rỗng, tương đương với kết quả mà bạn đang thấy. Khi sử dụng "wat" + 1thay vì "wat" - 1làm đối số, toán tử bổ sung chuyển đổi 1thành một chuỗi thay vì chuyển đổi "wat"thành một số, do đó, nó gọi một cách hiệu quả Array(16).join("wat1").

Về lý do tại sao bạn thấy các kết quả khác nhau cho {} + []trường hợp: Khi sử dụng nó làm đối số hàm, bạn buộc câu lệnh phải là ExpressionStatement , điều này khiến cho không thể phân tích {}thành khối trống, do đó, nó được phân tích thành một đối tượng trống nghĩa đen


2
Vậy tại sao [] +1 => "1" và [] -1 => -1?
Rob Elsner

4
@RobElsner []+1khá nhiều theo cùng một logic như []+[], chỉ với 1.toString()toán hạng rhs. Để []-1xem giải thích về "wat"-1điểm 5. Hãy nhớ rằng đó ToNumber(ToPrimitive([]))là 0 (điểm 3).
Ventero

4
Giải thích này bị thiếu / bỏ sót rất nhiều chi tiết. Ví dụ: "chuyển đổi một đối tượng (trong trường hợp này là một mảng) thành một nguyên hàm trả về giá trị mặc định của nó, đối với các đối tượng có phương thức toString () hợp lệ là kết quả của việc gọi object.toString ()" hoàn toàn thiếu giá trị đó của [] được gọi đầu tiên, nhưng vì giá trị trả về không phải là nguyên thủy (nó là một mảng), nên toString của [] được sử dụng thay thế. Tôi khuyên bạn nên xem xét điều này thay vì giải thích sâu sắc thực sự 2ality.com/2012/01/object-plus-object.html
jahav

30

Đây là một nhận xét nhiều hơn là một câu trả lời, nhưng vì một số lý do tôi không thể nhận xét về câu hỏi của bạn. Tôi muốn sửa mã JSFiddle của bạn. Tuy nhiên, tôi đã đăng bài này trên Hacker News và ai đó đề nghị tôi đăng lại nó ở đây.

Vấn đề trong mã JSFiddle là ({})(mở dấu ngoặc trong dấu ngoặc đơn) không giống với {}(mở dấu ngoặc khi bắt đầu một dòng mã). Vì vậy, khi bạn gõ, out({} + [])bạn buộc {}phải trở thành một cái gì đó không phải là khi bạn gõ{} + [] . Đây là một phần của toàn bộ tính năng của Javascript.

Ý tưởng cơ bản là JavaScript đơn giản muốn cho phép cả hai hình thức này:

if (u)
    v;

if (x) {
    y;
    z;
}

Để làm như vậy, hai cách hiểu đã được thực hiện cho cú đúp mở đầu: 1. không bắt buộc và 2. nó có thể xuất hiện ở bất cứ đâu .

Đây là một động thái sai lầm. Mã thực không có dấu ngoặc mở xuất hiện ở giữa hư không và mã thực cũng có xu hướng dễ vỡ hơn khi sử dụng dạng thứ nhất thay vì dạng thứ hai. (Khoảng một tháng một lần trong công việc cuối cùng của tôi, tôi được gọi đến bàn của đồng nghiệp khi sửa đổi mã của tôi không hoạt động và vấn đề là họ đã thêm một dòng vào "nếu" mà không thêm xoăn Cuối cùng tôi cũng chấp nhận thói quen niềng răng xoăn luôn luôn được yêu cầu, ngay cả khi bạn chỉ viết một dòng.)

May mắn thay, trong nhiều trường hợp, eval () sẽ sao chép toàn bộ tính năng của JavaScript. Mã JSFiddle nên đọc:

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>");

[Ngoài ra, đó là lần đầu tiên tôi viết document.writeln trong nhiều năm và tôi cảm thấy hơi bẩn khi viết bất cứ điều gì liên quan đến cả document.writeln () và eval ().]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- Tôi không đồng ý (loại): Tôi thường có trong các khối được sử dụng trong quá khứ như thế này để biến phạm vi trong C . Thói quen này đã được chọn từ lâu khi thực hiện nhúng C trong đó các biến trên ngăn xếp chiếm không gian, vì vậy nếu chúng không còn cần thiết, chúng tôi muốn không gian được giải phóng ở cuối khối. Tuy nhiên, ECMAScript chỉ phạm vi trong các khối hàm () {}. Vì vậy, trong khi tôi không đồng ý rằng khái niệm này là sai, tôi đồng ý rằng việc triển khai trong JS là ( có thể ) sai.
Jess Telford

4
@JessTelford Trong ES6, bạn có thể sử dụng letđể khai báo các biến trong phạm vi khối.
Oriol

19

Tôi thứ hai @ Ventero giải pháp. Nếu bạn muốn, bạn có thể đi vào chi tiết hơn như làm thế nào+ chuyển đổi toán hạng của nó.

Bước đầu tiên (§9.1): chuyển đổi cả hai toán hạng để nguyên thủy (giá trị nguyên thủy là undefined, null, boolean, số, chuỗi, tất cả các giá trị khác là các đối tượng, bao gồm các mảng và các hàm). Nếu một toán hạng đã nguyên thủy, bạn đã hoàn thành. Nếu không, nó là một đối tượng objvà các bước sau đây được thực hiện:

  1. Gọi obj.valueOf(). Nếu nó trả về một nguyên thủy, bạn đã hoàn thành. Trường hợp trực tiếp củaObject và mảng tự trả về, vì vậy bạn chưa hoàn thành.
  2. Gọi obj.toString(). Nếu nó trả về một nguyên thủy, bạn đã hoàn thành. {}[]cả hai trả về một chuỗi, vậy là xong.
  3. Nếu không, ném a TypeError.

Đối với ngày, bước 1 và 2 được hoán đổi. Bạn có thể quan sát hành vi chuyển đổi như sau:

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

Tương tác ( Number()đầu tiên chuyển đổi thành nguyên thủy sau đó thành số):

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

Bước thứ hai (§11.6.1): Nếu một trong các toán hạng là một chuỗi, toán hạng khác cũng được chuyển đổi thành chuỗi và kết quả được tạo ra bằng cách nối hai chuỗi. Mặt khác, cả hai toán hạng được chuyển đổi thành số và kết quả được tạo ra bằng cách thêm chúng.

Giải thích chi tiết hơn về quá trình chuyển đổi: Hoài {} + {} trong JavaScript là gì? Giáo dục


13

Chúng tôi có thể đề cập đến đặc điểm kỹ thuật và đó là điều tuyệt vời và chính xác nhất, nhưng hầu hết các trường hợp cũng có thể được giải thích theo cách dễ hiểu hơn với các tuyên bố sau:

  • +và các -toán tử chỉ làm việc với các giá trị nguyên thủy. Cụ thể hơn +(bổ sung) hoạt động với cả chuỗi hoặc số, và +(unary) và -(trừ và unary) chỉ hoạt động với số.
  • Tất cả các hàm hoặc toán tử riêng dự kiến ​​giá trị nguyên thủy là đối số, trước tiên sẽ chuyển đổi đối số đó thành kiểu nguyên thủy mong muốn. Nó được thực hiện với valueOfhoặc toString, có sẵn trên bất kỳ đối tượng. Đó là lý do tại sao các hàm hoặc toán tử như vậy không ném lỗi khi được gọi trên các đối tượng.

Vì vậy, chúng tôi có thể nói rằng:

  • [] + []là giống như String([]) + String([])đó là giống như '' + ''. Tôi đã đề cập ở trên rằng +(bổ sung) cũng hợp lệ cho các số, nhưng không có biểu diễn số hợp lệ của một mảng trong JavaScript, vì vậy việc thêm chuỗi được sử dụng thay thế.
  • [] + {}giống như String([]) + String({})cái giống như'' + '[object Object]'
  • {} + []. Điều này xứng đáng được giải thích nhiều hơn (xem câu trả lời của Ventero). Trong trường hợp đó, dấu ngoặc nhọn được coi không phải là một đối tượng mà là một khối trống, vì vậy nó hóa ra giống như +[]. Unary +chỉ hoạt động với các số, vì vậy việc thực hiện cố gắng lấy một số ra []. Đầu tiên, nó cố gắng valueOftrong trường hợp các mảng trả về cùng một đối tượng, do đó, nó sẽ thử phương án cuối cùng: chuyển đổi toStringkết quả thành một số. Chúng tôi có thể viết nó +Number(String([]))giống như +Number('')cái giống như+0 .
  • Array(16).join("wat" - 1)phép trừ -chỉ hoạt động với các số, vì vậy, nó giống như:, Array(16).join(Number("wat") - 1)"wat"không thể chuyển đổi thành số hợp lệ. Chúng tôi nhận được NaNvà bất kỳ hoạt động số học nào về NaNkết quả NaN, vì vậy chúng tôi có : Array(16).join(NaN).

0

Để làm chủ những gì đã được chia sẻ trước đó.

Nguyên nhân cơ bản của hành vi này một phần là do tính chất yếu của JavaScript. Ví dụ: biểu thức 1 + phiên bản 2 không rõ ràng vì có hai cách hiểu có thể dựa trên các loại toán hạng (int, chuỗi) và (int int):

  • Người dùng dự định nối hai chuỗi, kết quả:
  • Người dùng dự định thêm hai số, kết quả: 3

Do đó với các loại đầu vào khác nhau, khả năng đầu ra tăng lên.

Thuật toán bổ sung

  1. Ép buộc toán hạng đến giá trị nguyên thủy

Các nguyên hàm JavaScript là chuỗi, số, null, không xác định và boolean (Biểu tượng sẽ sớm xuất hiện trong ES6). Bất kỳ giá trị nào khác là một đối tượng (ví dụ: mảng, hàm và đối tượng). Quá trình cưỡng chế để chuyển đổi các đối tượng thành các giá trị nguyên thủy được mô tả như sau:

  • Nếu một giá trị nguyên thủy được trả về khi object.valueOf () được gọi, thì trả về giá trị này, nếu không thì tiếp tục

  • Nếu một giá trị nguyên thủy được trả về khi object.toString () được gọi, sau đó trả về giá trị này, nếu không tiếp tục

  • Ném TypeError

Lưu ý: Đối với các giá trị ngày, thứ tự là gọi toString trước valueOf.

  1. Nếu bất kỳ giá trị toán hạng nào là một chuỗi, thì thực hiện nối chuỗi

  2. Mặt khác, chuyển đổi cả hai toán hạng thành giá trị số của chúng và sau đó thêm các giá trị này

Biết các giá trị cưỡng chế khác nhau của các loại trong JavaScript sẽ giúp làm cho các đầu ra khó hiểu trở nên rõ ràng hơn. Xem bảng cưỡng chế dưới đây

+-----------------+-------------------+---------------+
| 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           |
+-----------------+-------------------+---------------+

Cũng rất tốt để biết rằng toán tử + của JavaScript là liên kết trái vì điều này xác định đầu ra sẽ là những trường hợp liên quan đến nhiều hơn một thao tác +.

Tận dụng Do đó 1 + "2" sẽ cho "12" bởi vì bất kỳ bổ sung nào liên quan đến một chuỗi sẽ luôn được mặc định là nối chuỗi.

Bạn có thể đọc thêm các ví dụ trong bài đăng trên blog này (từ chối trách nhiệm tôi đã viết nó).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.