Membuat rentang dalam JavaScript - sintaks aneh

129

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?

Benjamin Gruenbaum
sumber
2
IMO Array.apply(null, Array(30)).map(Number.call, Number)lebih mudah dibaca karena menghindari pura-pura bahwa objek biasa adalah Array.
fncomp
10
@ fncomp Tolong jangan gunakan salah satu untuk benar - benar membuat rentang. Tidak hanya lebih lambat dari pada pendekatan langsung - itu juga tidak mudah untuk dipahami. Sulit untuk memahami sintaks (baik, benar-benar API dan bukan sintaks) di sini yang membuat ini pertanyaan yang menarik tetapi IMO kode produksi yang mengerikan.
Benjamin Gruenbaum
Ya, tidak menyarankan siapa pun menggunakannya, tetapi berpikir itu masih lebih mudah dibaca, relatif terhadap objek versi literal.
fncomp
1
Saya tidak yakin mengapa ada orang yang mau melakukan ini. Jumlah waktu yang diperlukan untuk membuat array dengan cara ini bisa dilakukan dengan cara yang sedikit kurang seksi tapi jauh lebih cepat: jsperf.com/basic-vs-extreme
Eric Hodonsky

Jawaban:

263

Memahami "peretasan" ini membutuhkan pemahaman beberapa hal:

  1. Kenapa kita tidak melakukannya saja Array(5).map(...)
  2. Bagaimana Function.prototype.applymenangani argumen
  3. Bagaimana Arraymenangani beberapa argumen
  4. Bagaimana Numberfungsinya menangani argumen
  5. Apa Function.prototype.calltidak

Mereka 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 lengthvariabel magis , tetapi pada intinya, ini adalah key => valuepeta 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=>valuepemetaan yang dimiliki array, yang bisa berbeda dari arr.length.

Memperluas array via arr.length tidak membuat key=>valuepemetaan 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.maptidak 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.toUpperCaseakan 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 lenmerupakan uint32 yang valid, dan bukan sembarang jumlah nilai)

Jadi sekarang Anda dapat melihat mengapa melakukan Array(5).map(...)tidak akan berhasil - kami tidak mendefinisikan lenitem pada array, kami tidak membuat key => valuepemetaan, kami hanya mengubah lengthproperti.

Sekarang kita memiliki hal itu, mari kita lihat hal ajaib kedua:

2. Bagaimana cara Function.prototype.applykerjanya

Apa yang applydilakukan 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 applykerjanya dengan hanya mencatat argumentsvariabel 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 => valuepemetaan mungkin tidak ada dalam array kami melewati ke apply, tapi jelas ada dalam argumentsvariabel. 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.applydidefinisikan. Sebagian besar hal yang tidak kita pedulikan, tapi inilah bagian yang menarik:

  1. Mari kita menjadi hasil memanggil metode internal [[Get]] argArray dengan argumen "length".

Yang pada dasarnya berarti: argArray.length. Spec kemudian melanjutkan untuk melakukan forloop sederhana ke lengthitem, membuat listnilai yang sesuai ( listadalah 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 argArraydalam hal ini adalah objek dengan lengthproperti. Dan sekarang kita bisa melihat mengapa nilainya tidak terdefinisi, tetapi kuncinya tidak, pada arguments: Kami membuat key=>valuepemetaan.

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

3. Bagaimana Arraymenangani beberapa argumen

Begitu! Kami melihat apa yang terjadi ketika Anda menyampaikan lengthargumen Array, tetapi dalam ekspresi, kami menyampaikan beberapa hal sebagai argumen ( undefinedtepatnya 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.

Bagian pertama dari ekspresi

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.

4. Bagaimana Numbermemperlakukan input

Melakukan Number(something)( bagian 15.7.1 ) dikonversi somethingke 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

calladalah applysaudara 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 callbersama-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.callhanya fungsi, setara dengan callmetode fungsi lain , dan karenanya, memiliki callmetode sendiri:

log.call === log.call.call; //true
log.call === Function.call; //true

Dan apa fungsinya call? Ia menerima a thisArgdan 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}])

Bagian selanjutnya, atau bagian .mapdari itu semua

Ini 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 thisargumen, 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 forEachdidefinisikan, 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.

Kesimpulannya,

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]
Zirak
sumber
@Zirak Tolong bantu saya dalam memahami hal-hal berikut ahaExclamationMark.apply(null, Array(2)); //2, true. Mengapa ia kembali 2dan truemasing - masing? Tidakkah Anda hanya menyampaikan satu argumen, yaitu di Array(2)sini?
Geek
4
@Geek Kami hanya meneruskan satu argumen apply, tetapi argumen itu "dipipihkan" menjadi dua argumen yang diteruskan ke fungsi. Anda dapat melihatnya dengan lebih mudah pada applycontoh pertama . Yang pertama console.logkemudian menunjukkan bahwa memang, kami menerima dua argumen (dua item array), dan yang kedua console.logmenunjukkan bahwa array memiliki key=>valuepemetaan di slot 1 (seperti yang dijelaskan di bagian 1 dari jawaban).
Zirak
4
Karena (beberapa) permintaan , Anda sekarang dapat menikmati versi audio: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak
1
Perhatikan bahwa meneruskan NodeList, yang merupakan objek host, ke metode asli seperti di 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.
RobG
2
Satu koreksi: thishanya default ke Windowdalam mode non-ketat.
ComFreek
21

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.

  • The thisnilainya nullyang tidak peduli untuk konstruktor Array ( thisadalah sama thisseperti dalam konteks sesuai dengan 15.3.4.3.2.a.
  • Kemudian new Arraydisebut lewat objek dengan lengthproperti - yang menyebabkan objek menjadi array seperti untuk semua yang penting .applykarena klausa berikut dalam .apply:
    • Mari kita menjadi hasil memanggil metode internal [[Get]] argArray dengan argumen "length".
  • Dengan demikian, .applylewat argumen dari 0 .length, karena memanggil [[Get]]pada { length: 5 }dengan nilai-nilai 0 sampai 4 hasil undefinedarray konstruktor disebut dengan lima argumen yang nilainya undefined(mendapatkan properti dideklarasikan dari objek).
  • Konstruktor array dipanggil dengan 0, 2 atau lebih argumen . Properti panjang array yang baru dibangun diatur ke jumlah argumen sesuai dengan spesifikasi dan nilai ke nilai yang sama.
  • Dengan demikian 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 undefineddan yang terakhir membuat array kosong dengan panjang 5. Khususnya, karena .mapperilaku (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.mapmemanggil fungsi callback (dalam hal ini Number.call) pada setiap elemen array dan menggunakan nilai yang ditentukan this(dalam hal ini mengatur thisnilai ke `Nomor).
  • Parameter kedua dari callback di peta (dalam hal ini Number.call) adalah indeks, dan yang pertama adalah nilai ini.
  • Ini berarti yang Numberdisebut dengan thissebagai undefined(nilai array) dan indeks sebagai parameter. Jadi pada dasarnya sama dengan memetakan masing undefined- masing ke indeks arraynya (karena panggilan Numbermelakukan 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.

Benjamin Gruenbaum
sumber
1
Untuk dokumen: Spesifikasi untuk cara kerja peta: es5.github.io/#x15.4.4.19 , Mozilla memiliki skrip sampel yang berfungsi sesuai dengan spesifikasi itu di developer.mozilla.org/en-US/docs/Web/JavaScript/ Referensi / ...
Patrick Evans
1
Tetapi mengapa itu hanya bekerja dengan Array.apply(null, { length: 2 })dan bukan Array.apply(null, [2])yang juga akan memanggil Arraykonstruktor 2sebagai nilai panjang? biola
Andreas
@Andreas Array.apply(null,[2])adalah seperti Array(2)yang menciptakan array kosong dengan panjang 2 dan bukan array yang mengandung nilai primitif undefineddua 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.
Benjamin Gruenbaum
Saya belum mengerti cara kerjanya pada jalankan pertama ... Setelah membaca kedua masuk akal. {length: 2}memalsukan array dengan dua elemen yang Arrayakan dimasukkan oleh konstruktor ke dalam array yang baru dibuat. Karena tidak ada array nyata mengakses elemen hasil tidak hadir undefinedyang kemudian dimasukkan. Trik yang bagus :)
Andreas
5

Seperti yang Anda katakan, bagian pertama:

var arr = Array.apply(null, { length: 5 }); 

menciptakan array 5 undefinednilai.

Bagian kedua memanggil mapfungsi array yang mengambil 2 argumen dan mengembalikan array baru dengan ukuran yang sama.

Argumen pertama yang mapdiambil 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

  • a sebagai nilai dari elemen iterated saat ini
  • b sebagai indeks dari elemen iterasi saat ini
  • c sebagai keseluruhan array asli

Argumen kedua yang mapdiambil sedang diteruskan ke fungsi yang Anda berikan sebagai argumen pertama. Tetapi itu tidak akan a, b, atau c jika fooitu 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 maplewat adalah indeks, nilai yang akan ditempatkan dalam array baru pada indeks itu sama dengan indeks. Sama seperti fungsi bazpada contoh di atas.Number.callakan mencoba mem-parsing indeks - secara alami akan mengembalikan nilai yang sama.

Argumen kedua yang Anda mapberikan ke fungsi dalam kode Anda sebenarnya tidak berpengaruh pada hasilnya. Tolong perbaiki saya jika saya salah.

Tal Z
sumber
1
Number.calltidak 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.
Bergi
0

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] === undefinedmenghasilkan 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

shex
sumber
1
Ada perbedaan besar antara [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.applydigunakan.
Bergi