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?
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?
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!
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 adalah key => value
peta biasa , sama seperti objek lainnya. Mari kita bermain dengan array sedikit, oke?
var arr = ['a', 'b', 'c'];
arr.hasOwnProperty(0); //true
arr[0]; //'a'
Object.keys(arr); //['0', '1', '2']
arr.length; //3, implies arr[3] === undefined
//we expand the array by 1 item
arr.length = 4;
arr[3]; //undefined
arr.hasOwnProperty(3); //false
Object.keys(arr); //['0', '1', '2']
Kita sampai pada perbedaan inheren antara jumlah item dalam array arr.length
,, dan jumlah key=>value
pemetaan yang dimiliki array, yang bisa berbeda dari arr.length
.
Memperluas array via arr.length
tidak membuat key=>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. Jika arr[3]
hanya tidak terdefinisi, dan kuncinya ada, semua fungsi array ini hanya akan pergi seperti nilai lainnya:
//just to remind you
arr; //['a', 'b', 'c', undefined];
arr.length; //4
arr[4] = 'e';
arr; //['a', 'b', 'c', undefined, 'e'];
arr.length; //5
Object.keys(arr); //['0', '1', '2', '4']
arr.map(function (item) { return item.toUpperCase() });
//["A", "B", "C", undefined, "E"]
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 :
arr[5] = undefined;
arr; //["a", "b", "c", undefined, "e", undefined]
arr.hasOwnProperty(5); //true
arr.map(function (item) { return item.toUpperCase() });
//TypeError: Cannot call method 'toUpperCase' of undefined
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:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(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 mendefinisikan len
item pada array, kami tidak membuat key => value
pemetaan, kami hanya mengubah length
properti.
Sekarang kita memiliki hal itu, mari kita lihat hal ajaib kedua:
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:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Sekarang, kita dapat mempermudah proses melihat cara apply
kerjanya dengan hanya mencatat arguments
variabel khusus:
function log () {
console.log(arguments);
}
log.apply(null, ['mary', 'had', 'a', 'little', 'lamb']);
//["mary", "had", "a", "little", "lamb"]
//arguments is a pseudo-array itself, so we can use it as well
(function () {
log.apply(null, arguments);
})('mary', 'had', 'a', 'little', 'lamb');
//["mary", "had", "a", "little", "lamb"]
//a NodeList, like the one returned from DOM methods, is also a pseudo-array
log.apply(null, document.getElementsByTagName('script'));
//[script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script, script]
//carefully look at the following two
log.apply(null, Array(5));
//[undefined, undefined, undefined, undefined, undefined]
//note that the above are not undefined keys - but the value undefined itself!
log.apply(null, {length : 5});
//[undefined, undefined, undefined, undefined, undefined]
Mudah untuk membuktikan klaim saya dalam contoh kedua hingga terakhir:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(Ya, pun intended). The key => value
pemetaan mungkin tidak ada dalam array kami melewati ke apply
, tapi jelas ada dalam arguments
variabel. Ini adalah alasan yang sama dengan contoh terakhir yang berfungsi: Kunci tidak ada pada objek yang kita lewati, tetapi mereka ada di arguments
.
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:
- Mari kita menjadi hasil memanggil metode internal [[Get]] argArray dengan argumen "length".
Yang pada dasarnya berarti: argArray.length
. Spec kemudian melanjutkan untuk melakukan for
loop sederhana ke length
item, membuat list
nilai yang sesuai ( list
adalah beberapa voodoo internal, tetapi pada dasarnya merupakan array). Dalam hal kode yang sangat, sangat longgar:
Function.prototype.apply = function (thisArg, argArray) {
var len = argArray.length,
argList = [];
for (var i = 0; i < len; i += 1) {
argList[i] = argArray[i];
}
//yeah...
superMagicalFunctionInvocation(this, thisArg, argList);
};
Jadi yang kita butuhkan untuk meniru argArray
dalam hal ini adalah objek dengan length
properti. Dan sekarang kita bisa melihat mengapa nilainya tidak terdefinisi, tetapi kuncinya tidak, pada arguments
: Kami membuat key=>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:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Array
menangani beberapa argumenBegitu! Kami melihat apa yang terjadi ketika Anda menyampaikan length
argumen Array
, 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:
function Array () {
var ret = [];
ret.length = arguments.length;
for (var i = 0; i < arguments.length; i += 1) {
ret[i] = arguments[i];
}
return ret;
}
Array(0, 1, 2); //[0, 1, 2]
Array.apply(null, [0, 1, 2]); //[0, 1, 2]
Array.apply(null, Array(2)); //[undefined, undefined]
Array.apply(null, {length:2}); //[undefined, undefined]
Tada! Kami mendapatkan array dari beberapa nilai yang tidak ditentukan, dan kami mengembalikan array dari nilai-nilai yang tidak terdefinisi ini.
Akhirnya, kita dapat menguraikan hal-hal berikut:
Array.apply(null, { length: 5 })
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:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Ini akan menjadi bagian yang lebih mudah dan tidak berbelit-belit, karena tidak terlalu bergantung pada peretasan yang tidak jelas.
Number
memperlakukan inputMelakukan Number(something)
( bagian 15.7.1 ) dikonversi something
ke angka, dan itu saja. Cara melakukannya agak berbelit-belit, terutama dalam kasus string, tetapi operasi didefinisikan pada bagian 9.3 jika Anda tertarik.
Function.prototype.call
call
adalah apply
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:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Ini cukup layak sampai Anda memahami apa yang sedang terjadi. log.call
hanya fungsi, setara dengan call
metode fungsi lain , dan karenanya, memiliki call
metode sendiri:
log.call === log.call.call; //true
log.call === Function.call; //true
Dan apa fungsinya call
? Ia menerima a thisArg
dan banyak argumen, dan memanggil fungsi induknya. Kita dapat mendefinisikannya melalui apply
(sekali lagi, kode yang sangat longgar, tidak akan berfungsi):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Mari kita lacak bagaimana ini turun:
log.call.call(log, {a:4}, {a:5});
this = log.call
thisArg = log
args = [{a:4}, {a:5}]
log.call.apply(log, [{a:4}, {a:5}])
log.call({a:4}, {a:5})
this = log
thisArg = {a:4}
args = [{a:5}]
log.apply({a:4}, [{a:5}])
.map
dari itu semuaIni belum selesai. Mari kita lihat apa yang terjadi ketika Anda menyediakan fungsi ke sebagian besar metode array:
function log () {
console.log(this, arguments);
}
var arr = ['a', 'b', 'c'];
arr.forEach(log);
//window, ['a', 0, ['a', 'b', 'c']]
//window, ['b', 1, ['a', 'b', 'c']]
//window, ['c', 2, ['a', 'b', 'c']]
//^----^ ^-----------------------^
// this arguments
Jika kami sendiri tidak memberikan this
argumen, defaultnya adalah window
. Catat urutan argumen yang disediakan untuk panggilan balik kami, dan mari kita ulangi sampai ke 11 lagi:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
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:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Jadi, kami mendapatkan ini:
log.call.call(log, arr[i], i, arr);
//After one `.call`, it cascades to:
log.call(arr[i], i, arr);
//Further cascading to:
log(i, arr);
Sekarang kita bisa melihat cara .map(Number.call, Number)
kerjanya:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Yang mengembalikan transformasi i
, indeks saat ini, ke angka.
Ekspresi
Array.apply(null, { length: 5 }).map(Number.call, Number);
Bekerja dalam dua bagian:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
Bagian pertama menciptakan array 5 item yang tidak ditentukan. Yang kedua melewati array itu dan mengambil indeksnya, menghasilkan sebuah array indeks elemen:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true
. Mengapa ia kembali 2
dan true
masing - masing? Tidakkah Anda hanya menyampaikan satu argumen, yaitu di Array(2)
sini?
apply
, tetapi argumen itu "dipipihkan" menjadi dua argumen yang diteruskan ke fungsi. Anda dapat melihatnya dengan lebih mudah pada apply
contoh pertama . Yang pertama console.log
kemudian menunjukkan bahwa memang, kami menerima dua argumen (dua item array), dan yang kedua console.log
menunjukkan bahwa array memiliki key=>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 ke Window
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.
var arr = Array.apply(null, { length: 5 }); // Create an array of 5 `undefined` values
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Di baris pertama, konstruktor array disebut sebagai fungsi dengan Function.prototype.apply
.
this
nilainya null
yang tidak peduli untuk konstruktor Array ( this
adalah sama this
seperti dalam konteks sesuai dengan 15.3.4.3.2.a.new Array
disebut lewat objek dengan length
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 hasil undefined
array konstruktor disebut dengan lima argumen yang nilainya undefined
(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})
dan Array(5)
, yang pertama menciptakan lima kali tipe nilai primitif undefined
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:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Sekarang ke bagian kedua.
Array.prototype.map
memanggil fungsi callback (dalam hal ini Number.call
) pada setiap elemen array dan menggunakan nilai yang ditentukan this
(dalam hal ini mengatur this
nilai ke `Nomor).Number.call
) adalah indeks, dan yang pertama adalah nilai ini.Number
disebut dengan this
sebagai undefined
(nilai array) dan indeks sebagai parameter. Jadi pada dasarnya sama dengan memetakan masing undefined
- masing ke indeks arraynya (karena panggilan Number
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.
Array.apply(null,[2])
adalah seperti Array(2)
yang menciptakan array kosong dengan panjang 2 dan bukan array yang mengandung nilai primitif undefined
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 yang Array
akan dimasukkan oleh konstruktor ke dalam array yang baru dibuat. Karena tidak ada array nyata mengakses elemen hasil tidak hadir undefined
yang kemudian dimasukkan. Trik yang bagus :)
Seperti yang Anda katakan, bagian pertama:
var arr = Array.apply(null, { length: 5 });
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:
function foo(a,b,c){
...
return ...
}
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 jika foo
itu terjadi this
.
Dua contoh:
function bar(a,b,c){
return this
}
var arr2 = [3,4,5]
var newArr2 = arr2.map(bar, 9);
//newArr2 is equal to [9,9,9]
function baz(a,b,c){
return b
}
var newArr3 = arr2.map(baz,9);
//newArr3 is equal to [0,1,2]
dan satu lagi hanya untuk membuatnya lebih jelas:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
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 fungsi baz
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.
Number.call
tidak ada fungsi khusus yang mem-parsing argumen ke angka. Itu adil === Function.prototype.call
. Hanya argumen kedua, fungsi yang diteruskan sebagai this
-nilai untuk call
, 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 (yaitu arr[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
[undefined, undefined, undefined, …]
dan new Array(n)
atau {length: n}
- yang terakhir jarang , yaitu mereka tidak memiliki elemen. Ini sangat relevan untuk map
, dan itulah sebabnya yang aneh Array.apply
digunakan.
Array.apply(null, Array(30)).map(Number.call, Number)
lebih mudah dibaca karena menghindari pura-pura bahwa objek biasa adalah Array.