Apa penjelasan untuk perilaku JavaScript aneh yang disebutkan dalam pembicaraan 'Wat' untuk CodeMash 2012?

753

The 'Wat' berbicara untuk CodeMash 2012 pada dasarnya menunjukkan sebuah kebiasaan aneh beberapa dengan Ruby dan JavaScript.

Saya telah membuat JSFiddle dari hasil di http://jsfiddle.net/fe479/9/ .

Perilaku khusus untuk JavaScript (karena saya tidak tahu Ruby) tercantum di bawah ini.

Saya menemukan di JSFiddle bahwa beberapa hasil saya tidak sesuai dengan yang ada di video, dan saya tidak yakin mengapa. Namun saya ingin tahu bagaimana penanganan JavaScript di belakang layar dalam setiap kasus.

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

Saya cukup ingin tahu tentang +operator ketika digunakan dengan array dalam JavaScript. Ini cocok dengan hasil video.

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

Ini cocok dengan hasil video. Apa yang terjadi di sini? Mengapa ini sebuah objek? Apa yang dilakukan +operator?

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

Ini tidak cocok dengan video. Video menunjukkan bahwa hasilnya adalah 0, sedangkan saya mendapatkan [Objek].

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

Ini juga tidak cocok dengan video, dan bagaimana cara menghasilkan variabel menghasilkan dua objek? Mungkin JSFiddle saya salah.

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

Melakukan wat + 1 menghasilkan wat1wat1wat1wat1...

Saya menduga ini hanya perilaku langsung yang mencoba mengurangi angka dari hasil string di NaN.

NibblyPig
sumber
4
{} + [] Pada dasarnya adalah satu-satunya yang rumit dan implementasi bergantung, seperti yang saya jelaskan di sini , karena itu tergantung pada diuraikan sebagai pernyataan atau sebagai ekspresi. Lingkungan apa yang Anda uji (saya mendapatkan 0 yang diharapkan di Firefow dan Chrome tetapi mendapat "[objek Objek]" di NodeJs)?
hugomg
1
Saya menjalankan Firefox 9.0.1 di windows 7, dan JSFiddle mengevaluasinya ke [Object]
NibblyPig
@missingno saya mendapatkan 0 di NodeJS REPL
OrangeDog
41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson
1
@missingno Diposting pertanyaan di sini , tetapi untuk {} + {}.
Ionică Bizău

Jawaban:

1480

Berikut daftar penjelasan untuk hasil yang Anda lihat (dan seharusnya dilihat). Referensi yang saya gunakan berasal dari standar ECMA-262 .

  1. [] + []

    Saat menggunakan operator tambahan, operan kiri dan kanan dikonversi menjadi primitif terlebih dahulu ( §11.6.1 ). Seperti pada §9.1 , mengonversi objek (dalam hal ini sebuah array) menjadi primitif mengembalikan nilai defaultnya, yang untuk objek dengan toString()metode yang valid adalah hasil dari pemanggilan object.toString()( §8.12.8 ). Untuk array, ini sama dengan memanggil array.join()( §15.4.4.2 ). Bergabung dengan array kosong menghasilkan string kosong, jadi langkah # 7 dari operator tambahan mengembalikan rangkaian dua string kosong, yang merupakan string kosong.

  2. [] + {}

    Mirip dengan [] + [], kedua operan dikonversi menjadi primitif terlebih dahulu. Untuk "Objek objek" (§15.2), ini lagi merupakan hasil dari pemanggilan object.toString(), yang untuk objek non-null, non-tidak terdefinisi adalah "[object Object]"( §15.2.4.2 ).

  3. {} + []

    Di {}sini tidak diuraikan sebagai objek, melainkan sebagai blok kosong ( §12.1 , setidaknya selama Anda tidak memaksa pernyataan itu menjadi ekspresi, tetapi lebih lanjut tentang itu nanti). Nilai pengembalian blok kosong adalah kosong, sehingga hasil dari pernyataan itu sama dengan +[]. +Operator unary ( §11.4.6 ) kembali ToNumber(ToPrimitive(operand)). Seperti yang sudah kita ketahui, ToPrimitive([])adalah string kosong, dan menurut §9.3.1 , ToNumber("")adalah 0.

  4. {} + {}

    Mirip dengan kasus sebelumnya, yang pertama {}diuraikan sebagai blok dengan nilai pengembalian kosong. Sekali lagi, +{}adalah sama dengan ToNumber(ToPrimitive({})), dan ToPrimitive({})yang "[object Object]"(lihat [] + {}). Jadi untuk mendapatkan hasil +{}, kita harus menerapkannya ToNumberpada string "[object Object]". Ketika mengikuti langkah-langkah dari §9.3.1 , kita mendapatkan NaNhasilnya:

    Jika tata bahasa tidak dapat menafsirkan String sebagai perluasan dari StringNumericLiteral , maka hasil ToNumber adalah NaN .

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

    Sesuai §15.4.1.1 dan §15.4.2.2 , Array(16)buat array baru dengan panjang 16. Untuk mendapatkan nilai argumen yang akan digabungkan, §11.6.2 langkah # 5 dan # 6 menunjukkan bahwa kita harus mengubah kedua operan menjadi nomor menggunakan ToNumber. ToNumber(1)hanya 1 ( §9.3 ), sedangkan ToNumber("wat")sekali lagi NaNsesuai §9.3.1 . Mengikuti langkah 7 dari §11.6.2 , §11.6.3 menentukan itu

    Jika salah satu operan adalah NaN , hasilnya adalah NaN .

    Jadi argumennya Array(16).joinadalah NaN. Mengikuti §15.4.4.5 ( Array.prototype.join), kita harus memanggil ToStringargumen, yaitu "NaN"( §9.8.1 ):

    Jika m adalah NaN , kembalikan String "NaN".

    Mengikuti langkah 10 pada §15.4.4.5 , kami mendapatkan 15 pengulangan dari rangkaian "NaN"dan string kosong, yang sama dengan hasil yang Anda lihat. Saat menggunakan "wat" + 1alih-alih "wat" - 1sebagai argumen, operator tambahan mengonversi 1ke string alih-alih mengonversi "wat"ke nomor, sehingga secara efektif memanggil Array(16).join("wat1").

Mengenai mengapa Anda melihat hasil yang berbeda untuk {} + []kasus ini: Saat menggunakannya sebagai argumen fungsi, Anda memaksakan pernyataan itu menjadi ExpressionStatement , yang membuatnya tidak mungkin untuk menguraikan {}sebagai blok kosong, jadi alih-alih diuraikan sebagai objek kosong harfiah.

Ventero
sumber
2
Jadi mengapa [] +1 => "1" dan [] -1 => -1?
Rob Elsner
4
@RobElsner []+1cukup banyak mengikuti logika yang sama seperti []+[], hanya dengan 1.toString()operan rhs. Untuk []-1melihat penjelasan "wat"-1pada poin 5. Ingat itu ToNumber(ToPrimitive([]))adalah 0 (poin 3).
Ventero
4
Penjelasan ini tidak ada / menghilangkan banyak detail. Misalnya "mengonversi objek (dalam hal ini sebuah array) ke primitif mengembalikan nilai defaultnya, yang untuk objek dengan metode toString () yang valid adalah hasil dari memanggil objek. ToString ()" benar-benar hilang karena valueOf of [] adalah disebut pertama, tetapi karena nilai pengembalian bukan primitif (ini adalah array), toString of [] digunakan sebagai gantinya. Saya akan merekomendasikan untuk melihat ini sebagai gantinya penjelasan mendalam nyata 2ality.com/2012/01/object-plus-object.html
jahav
30

Ini lebih merupakan komentar daripada jawaban, tetapi untuk beberapa alasan saya tidak dapat mengomentari pertanyaan Anda. Saya ingin memperbaiki kode JSFiddle Anda. Namun, saya memposting ini di Hacker News dan seseorang menyarankan agar saya memposting ulang di sini.

Masalah dalam kode JSFiddle adalah ({})(membuka kurung di dalam tanda kurung) tidak sama dengan {}(membuka kurung sebagai awal baris kode). Jadi, ketika Anda mengetik, out({} + [])Anda memaksa {}untuk menjadi sesuatu yang bukan saat Anda mengetik {} + []. Ini adalah bagian dari 'wat'-ness keseluruhan Javascript.

Ide dasarnya adalah JavaScript sederhana yang ingin memungkinkan kedua bentuk ini:

if (u)
    v;

if (x) {
    y;
    z;
}

Untuk melakukannya, dua interpretasi dibuat dari brace pembuka: 1. tidak diperlukan dan 2. dapat muncul di mana saja .

Ini langkah yang salah. Kode nyata tidak memiliki tanda kurung pembuka yang muncul di tengah-tengah dari mana, dan kode nyata juga cenderung lebih rapuh ketika menggunakan bentuk pertama daripada yang kedua. (Sekitar sebulan sekali di pekerjaan terakhir saya, saya akan dipanggil ke meja rekan kerja ketika modifikasi mereka pada kode saya tidak berfungsi, dan masalahnya adalah mereka menambahkan baris ke "jika" tanpa menambahkan keriting Saya akhirnya hanya mengadopsi kebiasaan bahwa kurung kurawal selalu diperlukan, bahkan ketika Anda hanya menulis satu baris.)

Untungnya dalam banyak kasus eval () akan mereplikasi wat-ness penuh JavaScript. Kode JSFiddle harus dibaca:

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

[Juga itu adalah pertama kalinya saya menulis dokumen.writeln dalam banyak bertahun-tahun, dan saya merasa sedikit kotor menulis apa pun yang melibatkan kedua dokumen.writeln () dan eval ().]

CR Drost
sumber
15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere- Saya tidak setuju (semacam): Saya sudah sering di blok digunakan masa lalu seperti ini untuk variabel lingkup di C . Kebiasaan ini diambil untuk beberapa waktu yang lalu ketika melakukan embedded C di mana variabel pada stack mengambil ruang, jadi jika mereka tidak lagi diperlukan, kami ingin ruang dibebaskan di akhir blok. Namun, ECMAScript hanya lingkup dalam blok fungsi () {}. Jadi, sementara saya tidak setuju bahwa konsepnya salah, saya setuju bahwa implementasi di JS ( mungkin ) salah.
Jess Telford
4
@ JessTelford Di ES6, Anda dapat menggunakan letuntuk mendeklarasikan variabel blok-dicakup.
Oriol
19

Saya solusi kedua @ Ventero. Jika Anda mau, Anda bisa masuk ke lebih detail tentang bagaimana +mengkonversi operan.

Langkah pertama (§9.1): mengkonversi kedua operan ke primitif (nilai-nilai primitif yang undefined, null, boolean, angka, string, semua nilai-nilai lain adalah objek, termasuk array dan fungsi). Jika operan sudah primitif, Anda selesai. Jika tidak, itu adalah objek objdan langkah-langkah berikut dilakukan:

  1. Panggil obj.valueOf(). Jika mengembalikan primitif, Anda selesai. Contoh langsung dari Objectdan array kembali sendiri, sehingga Anda belum selesai.
  2. Panggil obj.toString(). Jika mengembalikan primitif, Anda selesai. {}dan[] keduanya mengembalikan string, sehingga Anda selesai.
  3. Kalau tidak, lempar a TypeError.

Untuk tanggal, langkah 1 dan 2 ditukar. Anda dapat mengamati perilaku konversi sebagai berikut:

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

Interaksi ( Number()mula-mula dikonversi ke primitif lalu ke nomor):

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

Langkah kedua (§11.6.1): Jika salah satu operan adalah string, operan lainnya juga dikonversi menjadi string dan hasilnya dihasilkan dengan menggabungkan dua string. Jika tidak, kedua operan dikonversikan menjadi angka dan hasilnya dihasilkan dengan menambahkannya.

Penjelasan lebih rinci tentang proses konversi: “ Apa itu {} + {} dalam JavaScript?

Axel Rauschmayer
sumber
13

Kami dapat merujuk pada spesifikasi dan itu hebat dan paling akurat, tetapi sebagian besar kasus juga dapat dijelaskan dengan cara yang lebih komprehensif dengan pernyataan berikut:

  • +dan -operator hanya bekerja dengan nilai primitif. Lebih khusus +(penambahan) bekerja dengan string atau angka, dan +(unary) dan -(pengurangan dan unary) hanya bekerja dengan angka.
  • Semua fungsi asli atau operator yang mengharapkan nilai primitif sebagai argumen, pertama-tama akan mengkonversi argumen itu ke tipe primitif yang diinginkan. Ini dilakukan dengan valueOfatau toString, yang tersedia pada objek apa pun. Itulah alasan mengapa fungsi atau operator tidak melakukan kesalahan ketika dipanggil pada objek.

Jadi kita dapat mengatakan bahwa:

  • [] + []sama seperti String([]) + String([])yang sama dengan '' + ''. Saya sebutkan di atas bahwa +(penambahan) juga berlaku untuk angka, tetapi tidak ada representasi angka yang valid dari sebuah array dalam JavaScript, jadi penambahan string digunakan sebagai gantinya.
  • [] + {}sama seperti String([]) + String({})yang sama dengan'' + '[object Object]'
  • {} + []. Yang ini layak mendapat penjelasan lebih lanjut (lihat jawaban Ventero). Dalam hal ini, kurung kurawal diperlakukan bukan sebagai objek tetapi sebagai blok kosong, sehingga ternyata sama dengan +[]. Unary +hanya bekerja dengan angka, sehingga implementasi mencoba untuk mendapatkan nomor dari []. Pertama ia mencoba valueOfyang dalam hal array mengembalikan objek yang sama, maka ia mencoba upaya terakhir: konversi toStringhasil ke angka. Kita dapat menuliskannya +Number(String([]))sama dengan +Number('')yang sama +0.
  • Array(16).join("wat" - 1)pengurangan -hanya bekerja dengan angka, jadi sama seperti: Array(16).join(Number("wat") - 1), seperti "wat"tidak dapat dikonversi ke angka yang benar. Kami menerima NaN, dan setiap operasi aritmatika pada NaNhasil dengan NaN, jadi kita harus: Array(16).join(NaN).
Mariusz Nowak
sumber
0

Untuk menopang apa yang telah dibagikan sebelumnya.

Penyebab yang mendasari perilaku ini sebagian karena sifat JavaScript yang diketik dengan lemah. Sebagai contoh, ekspresi 1 + “2” adalah ambigu karena ada dua kemungkinan interpretasi berdasarkan tipe operan (int, string) dan (int int):

  • Pengguna bermaksud menggabungkan dua string, menghasilkan: "12"
  • Pengguna bermaksud menambahkan dua angka, hasil: 3

Jadi dengan berbagai jenis input, kemungkinan output meningkat.

Algoritma penambahan

  1. Operan komersial ke nilai primitif

Primitif JavaScript adalah string, angka, null, tidak terdefinisi dan boolean (Simbol akan segera hadir di ES6). Nilai lain adalah objek (misalnya array, fungsi dan objek). Proses pemaksaan untuk mengubah objek menjadi nilai-nilai primitif dijelaskan sebagai berikut:

  • Jika nilai primitif dikembalikan ketika object.valueOf () dipanggil, maka kembalikan nilai ini, jika tidak lanjutkan

  • Jika nilai primitif dikembalikan ketika object.toString () dipanggil, maka kembalikan nilai ini, jika tidak lanjutkan

  • Lempar TypeError

Catatan: Untuk nilai tanggal, perintah ini untuk memanggil toString sebelum valueOf.

  1. Jika nilai operan adalah string, maka lakukan penggabungan string

  2. Jika tidak, konversikan kedua operan ke nilai numeriknya dan kemudian tambahkan nilai-nilai ini

Mengetahui berbagai nilai paksaan dari jenis-jenis dalam JavaScript memang membantu membuat hasil yang membingungkan menjadi lebih jelas. Lihat tabel paksaan di bawah ini

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

Juga baik untuk mengetahui bahwa operator + JavaScript adalah asosiatif kiri karena ini menentukan apa yang akan menjadi kasus yang melibatkan lebih dari satu operasi +.

Leveraging the Jadi 1 + "2" akan memberikan "12" karena penambahan apa pun yang melibatkan string akan selalu default ke rangkaian string.

Anda dapat membaca lebih banyak contoh di posting blog ini (disclaimer I wrote it).

AbdulFattah Popoola
sumber