Saya telah menemukan kode berikut di milis es-diskusi:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Ini menghasilkan
[0, 1, 2, 3, 4]
Mengapa ini hasil dari kode? Apa yang sedang terjadi disini?
javascript
ecmascript-5
Benjamin Gruenbaum
sumber
sumber
Array.apply(null, Array(30)).map(Number.call, Number)
lebih mudah dibaca karena menghindari pura-pura bahwa objek biasa adalah Array.Jawaban:
Memahami "peretasan" ini membutuhkan pemahaman beberapa hal:
Array(5).map(...)
Function.prototype.apply
menangani argumenArray
menangani beberapa argumenNumber
fungsinya menangani argumenFunction.prototype.call
tidakMereka adalah topik yang agak canggih dalam javascript, jadi ini akan lebih panjang. Kami akan mulai dari atas. Sabuk pengaman!
1. Mengapa tidak adil
Array(5).map
?Apa itu array, sungguh? Objek reguler, berisi kunci integer, yang memetakan ke nilai. Ini memiliki fitur-fitur khusus lainnya, misalnya
length
variabel magis , tetapi pada intinya, ini adalahkey => value
peta biasa , sama seperti objek lainnya. Mari kita bermain dengan array sedikit, oke?Kita sampai pada perbedaan inheren antara jumlah item dalam array
arr.length
,, dan jumlahkey=>value
pemetaan yang dimiliki array, yang bisa berbeda dariarr.length
.Memperluas array via
arr.length
tidak membuatkey=>value
pemetaan baru , jadi bukan karena array memiliki nilai yang tidak ditentukan, array tidak memiliki kunci-kunci ini . Dan apa yang terjadi ketika Anda mencoba mengakses properti yang tidak ada? Anda mendapatkanundefined
.Sekarang kita bisa sedikit mengangkat kepala kita, dan melihat mengapa fungsi seperti
arr.map
tidak berjalan di atas properti ini. Jikaarr[3]
hanya tidak terdefinisi, dan kuncinya ada, semua fungsi array ini hanya akan pergi seperti nilai lainnya:Saya sengaja menggunakan pemanggilan metode untuk membuktikan lebih lanjut bahwa kunci itu sendiri tidak pernah ada: Memanggil
undefined.toUpperCase
akan menimbulkan kesalahan, tetapi ternyata tidak. Untuk membuktikan itu :Dan sekarang kita sampai pada poin saya: Bagaimana
Array(N)
melakukan sesuatu. Bagian 15.4.2.2 menjelaskan prosesnya. Ada banyak omong kosong yang tidak kita pedulikan, tetapi jika Anda berhasil membaca yang tersirat (atau Anda bisa mempercayai saya tentang hal ini, tetapi tidak), pada dasarnya bermuara pada ini:(beroperasi berdasarkan asumsi (yang diperiksa dalam spesifikasi aktual) yang
len
merupakan uint32 yang valid, dan bukan sembarang jumlah nilai)Jadi sekarang Anda dapat melihat mengapa melakukan
Array(5).map(...)
tidak akan berhasil - kami tidak mendefinisikanlen
item pada array, kami tidak membuatkey => value
pemetaan, kami hanya mengubahlength
properti.Sekarang kita memiliki hal itu, mari kita lihat hal ajaib kedua:
2. Bagaimana cara
Function.prototype.apply
kerjanyaApa yang
apply
dilakukan pada dasarnya adalah mengambil sebuah array, dan membuka gulungannya sebagai argumen pemanggilan fungsi. Itu berarti bahwa yang berikut hampir sama:Sekarang, kita dapat mempermudah proses melihat cara
apply
kerjanya dengan hanya mencatatarguments
variabel khusus:Mudah untuk membuktikan klaim saya dalam contoh kedua hingga terakhir:
(Ya, pun intended). The
key => value
pemetaan mungkin tidak ada dalam array kami melewati keapply
, tapi jelas ada dalamarguments
variabel. Ini adalah alasan yang sama dengan contoh terakhir yang berfungsi: Kunci tidak ada pada objek yang kita lewati, tetapi mereka ada diarguments
.Mengapa demikian? Mari kita lihat Bagian 15.3.4.3 , di mana
Function.prototype.apply
didefinisikan. Sebagian besar hal yang tidak kita pedulikan, tapi inilah bagian yang menarik:Yang pada dasarnya berarti:
argArray.length
. Spec kemudian melanjutkan untuk melakukanfor
loop sederhana kelength
item, membuatlist
nilai yang sesuai (list
adalah beberapa voodoo internal, tetapi pada dasarnya merupakan array). Dalam hal kode yang sangat, sangat longgar:Jadi yang kita butuhkan untuk meniru
argArray
dalam hal ini adalah objek denganlength
properti. Dan sekarang kita bisa melihat mengapa nilainya tidak terdefinisi, tetapi kuncinya tidak, padaarguments
: Kami membuatkey=>value
pemetaan.Fiuh, jadi ini mungkin tidak lebih pendek dari bagian sebelumnya. Tapi akan ada kue saat kita selesai, jadi bersabarlah! Namun, setelah bagian berikut (yang akan singkat, saya berjanji) kita dapat mulai membedah ekspresi. Jika Anda lupa, pertanyaannya adalah bagaimana cara kerja berikut ini:
3. Bagaimana
Array
menangani beberapa argumenBegitu! Kami melihat apa yang terjadi ketika Anda menyampaikan
length
argumenArray
, tetapi dalam ekspresi, kami menyampaikan beberapa hal sebagai argumen (undefined
tepatnya array 5 ). Bagian 15.4.2.1 memberi tahu kita apa yang harus dilakukan. Paragraf terakhir adalah yang terpenting bagi kami, dan itu bernada benar-benar aneh, tapi itu jenis bermuara:Tada! Kami mendapatkan array dari beberapa nilai yang tidak ditentukan, dan kami mengembalikan array dari nilai-nilai yang tidak terdefinisi ini.
Bagian pertama dari ekspresi
Akhirnya, kita dapat menguraikan hal-hal berikut:
Kami melihat bahwa ia mengembalikan array yang berisi 5 nilai yang tidak ditentukan, dengan kunci-kunci yang semuanya ada.
Sekarang, ke bagian kedua dari ungkapan:
Ini akan menjadi bagian yang lebih mudah dan tidak berbelit-belit, karena tidak terlalu bergantung pada peretasan yang tidak jelas.
4. Bagaimana
Number
memperlakukan inputMelakukan
Number(something)
( bagian 15.7.1 ) dikonversisomething
ke angka, dan itu saja. Cara melakukannya agak berbelit-belit, terutama dalam kasus string, tetapi operasi didefinisikan pada bagian 9.3 jika Anda tertarik.5. Game dari
Function.prototype.call
call
adalahapply
saudara laki-laki, didefinisikan dalam bagian 15.3.4.4 . Alih-alih mengambil array argumen, itu hanya mengambil argumen yang diterima, dan meneruskannya.Hal-hal menjadi menarik ketika Anda rantai lebih dari satu
call
bersama-sama, engkol aneh hingga 11:Ini cukup layak sampai Anda memahami apa yang sedang terjadi.
log.call
hanya fungsi, setara dengancall
metode fungsi lain , dan karenanya, memilikicall
metode sendiri:Dan apa fungsinya
call
? Ia menerima athisArg
dan banyak argumen, dan memanggil fungsi induknya. Kita dapat mendefinisikannya melaluiapply
(sekali lagi, kode yang sangat longgar, tidak akan berfungsi):Mari kita lacak bagaimana ini turun:
Bagian selanjutnya, atau bagian
.map
dari itu semuaIni belum selesai. Mari kita lihat apa yang terjadi ketika Anda menyediakan fungsi ke sebagian besar metode array:
Jika kami sendiri tidak memberikan
this
argumen, defaultnya adalahwindow
. Catat urutan argumen yang disediakan untuk panggilan balik kami, dan mari kita ulangi sampai ke 11 lagi:Whoa whoa whoa ... mari kita mundur sedikit. Apa yang terjadi di sini? Kita dapat melihat di bagian 15.4.4.18 , di mana
forEach
didefinisikan, berikut ini cukup banyak terjadi:Jadi, kami mendapatkan ini:
Sekarang kita bisa melihat cara
.map(Number.call, Number)
kerjanya:Yang mengembalikan transformasi
i
, indeks saat ini, ke angka.Kesimpulannya,
Ekspresi
Bekerja dalam dua bagian:
Bagian pertama menciptakan array 5 item yang tidak ditentukan. Yang kedua melewati array itu dan mengambil indeksnya, menghasilkan sebuah array indeks elemen:
sumber
ahaExclamationMark.apply(null, Array(2)); //2, true
. Mengapa ia kembali2
dantrue
masing - masing? Tidakkah Anda hanya menyampaikan satu argumen, yaitu diArray(2)
sini?apply
, tetapi argumen itu "dipipihkan" menjadi dua argumen yang diteruskan ke fungsi. Anda dapat melihatnya dengan lebih mudah padaapply
contoh pertama . Yang pertamaconsole.log
kemudian menunjukkan bahwa memang, kami menerima dua argumen (dua item array), dan yang keduaconsole.log
menunjukkan bahwa array memilikikey=>value
pemetaan di slot 1 (seperti yang dijelaskan di bagian 1 dari jawaban).log.apply(null, document.getElementsByTagName('script'));
tidak diperlukan untuk bekerja dan tidak bekerja di beberapa browser, dan[].slice.call(NodeList)
untuk mengubah NodeList menjadi sebuah array tidak akan bekerja di dalamnya juga.this
hanya default keWindow
dalam mode non-ketat.Penafian : Ini adalah deskripsi yang sangat formal dari kode di atas - ini adalah bagaimana saya tahu bagaimana menjelaskannya. Untuk jawaban yang lebih sederhana - lihat jawaban hebat Zirak di atas. Ini adalah spesifikasi yang lebih mendalam di wajah Anda dan lebih sedikit "aha".
Beberapa hal sedang terjadi di sini. Mari kita hancurkan sedikit.
Di baris pertama, konstruktor array disebut sebagai fungsi dengan
Function.prototype.apply
.this
nilainyanull
yang tidak peduli untuk konstruktor Array (this
adalah samathis
seperti dalam konteks sesuai dengan 15.3.4.3.2.a.new Array
disebut lewat objek denganlength
properti - yang menyebabkan objek menjadi array seperti untuk semua yang penting.apply
karena klausa berikut dalam.apply
:.apply
lewat argumen dari 0.length
, karena memanggil[[Get]]
pada{ length: 5 }
dengan nilai-nilai 0 sampai 4 hasilundefined
array konstruktor disebut dengan lima argumen yang nilainyaundefined
(mendapatkan properti dideklarasikan dari objek).var arr = Array.apply(null, { length: 5 });
membuat daftar lima nilai yang tidak ditentukan.Catatan : Perhatikan perbedaan di sini antara
Array.apply(0,{length: 5})
danArray(5)
, yang pertama menciptakan lima kali tipe nilai primitifundefined
dan yang terakhir membuat array kosong dengan panjang 5. Khususnya, karena.map
perilaku (8.b) dan secara khusus[[HasProperty]
.Jadi kode di atas dalam spesifikasi yang sesuai sama dengan:
Sekarang ke bagian kedua.
Array.prototype.map
memanggil fungsi callback (dalam hal iniNumber.call
) pada setiap elemen array dan menggunakan nilai yang ditentukanthis
(dalam hal ini mengaturthis
nilai ke `Nomor).Number.call
) adalah indeks, dan yang pertama adalah nilai ini.Number
disebut denganthis
sebagaiundefined
(nilai array) dan indeks sebagai parameter. Jadi pada dasarnya sama dengan memetakan masingundefined
- masing ke indeks arraynya (karena panggilanNumber
melakukan konversi jenis, dalam hal ini dari angka ke angka tidak mengubah indeks).Dengan demikian, kode di atas mengambil lima nilai yang tidak ditentukan dan memetakan masing-masing ke indeks dalam array.
Itulah sebabnya kami mendapatkan hasilnya ke kode kami.
sumber
Array.apply(null, { length: 2 })
dan bukanArray.apply(null, [2])
yang juga akan memanggilArray
konstruktor2
sebagai nilai panjang? biolaArray.apply(null,[2])
adalah sepertiArray(2)
yang menciptakan array kosong dengan panjang 2 dan bukan array yang mengandung nilai primitifundefined
dua kali. Lihat hasil edit saya yang terbaru di catatan setelah bagian pertama, beri tahu saya apakah sudah cukup jelas dan jika tidak saya akan menjelaskannya.{length: 2}
memalsukan array dengan dua elemen yangArray
akan dimasukkan oleh konstruktor ke dalam array yang baru dibuat. Karena tidak ada array nyata mengakses elemen hasil tidak hadirundefined
yang kemudian dimasukkan. Trik yang bagus :)Seperti yang Anda katakan, bagian pertama:
menciptakan array 5
undefined
nilai.Bagian kedua memanggil
map
fungsi array yang mengambil 2 argumen dan mengembalikan array baru dengan ukuran yang sama.Argumen pertama yang
map
diambil sebenarnya adalah fungsi untuk diterapkan pada setiap elemen dalam array, diharapkan menjadi fungsi yang mengambil 3 argumen dan mengembalikan nilai. Sebagai contoh:jika kita melewatkan fungsi foo sebagai argumen pertama maka akan dipanggil untuk setiap elemen dengan
Argumen kedua yang
map
diambil sedang diteruskan ke fungsi yang Anda berikan sebagai argumen pertama. Tetapi itu tidak akan a, b, atau c jikafoo
itu terjadithis
.Dua contoh:
dan satu lagi hanya untuk membuatnya lebih jelas:
Jadi bagaimana dengan Number.call?
Number.call
adalah fungsi yang mengambil 2 argumen, dan mencoba mengurai argumen kedua ke angka (saya tidak yakin apa yang dilakukannya dengan argumen pertama).Karena argumen kedua yang
map
lewat adalah indeks, nilai yang akan ditempatkan dalam array baru pada indeks itu sama dengan indeks. Sama seperti fungsibaz
pada contoh di atas.Number.call
akan mencoba mem-parsing indeks - secara alami akan mengembalikan nilai yang sama.Argumen kedua yang Anda
map
berikan ke fungsi dalam kode Anda sebenarnya tidak berpengaruh pada hasilnya. Tolong perbaiki saya jika saya salah.sumber
Number.call
tidak ada fungsi khusus yang mem-parsing argumen ke angka. Itu adil=== Function.prototype.call
. Hanya argumen kedua, fungsi yang diteruskan sebagaithis
-nilai untukcall
, relevan -.map(eval.call, Number)
,.map(String.call, Number)
dan.map(Function.prototype.call, Number)
semuanya setara.Array hanyalah sebuah objek yang terdiri dari bidang 'panjang' dan beberapa metode (misalnya push). Jadi arr pada
var arr = { length: 5}
dasarnya sama dengan array di mana field 0..4 memiliki nilai default yang tidak terdefinisi (yaituarr[0] === undefined
menghasilkan true).Adapun bagian kedua, peta, seperti namanya, peta dari satu array ke yang baru. Ia melakukannya dengan menelusuri array asli dan menjalankan fungsi pemetaan pada setiap item.
Yang tersisa adalah meyakinkan Anda bahwa hasil pemetaan-fungsi adalah indeks. Caranya adalah dengan menggunakan metode bernama 'call' (*) yang memanggil fungsi dengan pengecualian kecil bahwa param pertama diatur menjadi konteks 'this', dan yang kedua menjadi param pertama (dan seterusnya). Secara kebetulan, ketika fungsi pemetaan dipanggil, param kedua adalah indeks.
Last but not least, metode yang dipanggil adalah Number "Class", dan seperti yang kita tahu di JS, "Class" hanyalah sebuah fungsi, dan yang ini (Number) mengharapkan param pertama menjadi nilainya.
(*) ditemukan dalam prototipe Function (dan Number adalah fungsi).
MASHAL
sumber
[undefined, undefined, undefined, …]
dannew Array(n)
atau{length: n}
- yang terakhir jarang , yaitu mereka tidak memiliki elemen. Ini sangat relevan untukmap
, dan itulah sebabnya yang anehArray.apply
digunakan.