Ho incontrato il seguente codice nella mailing list di es-discuss:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Questo produce
[0, 1, 2, 3, 4]
Perché questo è il risultato del codice? Cosa sta succedendo qui?
Ho incontrato il seguente codice nella mailing list di es-discuss:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Questo produce
[0, 1, 2, 3, 4]
Perché questo è il risultato del codice? Cosa sta succedendo qui?
Risposte:
Comprendere questo "hack" richiede di capire diverse cose:
Array(5).map(...)Function.prototype.applygestisce gli argomentiArraygestisce più argomentiNumberfunzione gestisce gli argomentiFunction.prototype.callfaSono argomenti piuttosto avanzati in javascript, quindi questo sarà più che piuttosto lungo. Inizieremo dall'alto. Allacciati!
Array(5).map?Cos'è un array, davvero? Un oggetto normale, contenente chiavi intere, che si associano a valori. Ha altre caratteristiche speciali, ad esempio la lengthvariabile magica , ma al suo centro, è una key => valuemappa normale , proprio come qualsiasi altro oggetto. Giochiamo un po 'con le matrici, vero?
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']
Arriviamo alla differenza intrinseca tra il numero di elementi nell'array arr.lengthe il numero di key=>valuemappature dell'array, che può essere diverso da arr.length.
L'espansione dell'array tramite arr.length non crea nuovi key=>valuemapping, quindi non è che l'array abbia valori non definiti, non ha queste chiavi . E cosa succede quando si tenta di accedere a una proprietà inesistente? Hai capito undefined.
Ora possiamo sollevare un po 'la testa e capire perché funzioni come arr.mapnon calpestare queste proprietà. Se arr[3]fosse semplicemente indefinito, e la chiave esistesse, tutte queste funzioni dell'array andrebbero semplicemente su di essa come qualsiasi altro valore:
//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"]
Ho intenzionalmente usato una chiamata di metodo per dimostrare ulteriormente che la chiave stessa non era mai presente: la chiamata undefined.toUpperCaseavrebbe sollevato un errore, ma non è stato così. Per dimostrare che :
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
E ora arriviamo al mio punto: come Array(N)vanno le cose. La sezione 15.4.2.2 descrive il processo. C'è un sacco di jumbo mumbo di cui non ci importa, ma se riesci a leggere tra le righe (o puoi solo fidarti di me su questo, ma non farlo), in pratica si riduce a questo:
function Array(len) {
var ret = [];
ret.length = len;
return ret;
}
(opera in base al presupposto (che è verificato nelle specifiche effettive) che lenè un uint32 valido e non un numero qualsiasi di valori)
Quindi ora puoi capire perché il fare Array(5).map(...)non funziona: non definiamo lenelementi sull'array, non creiamo i key => valuemapping, semplicemente modifichiamo la lengthproprietà.
Ora che lo abbiamo rimosso, diamo un'occhiata alla seconda cosa magica:
Function.prototype.applyfunzionaQuello che applyfa è fondamentalmente prendere un array e srotolarlo come argomento di una chiamata di funzione. Ciò significa che quanto segue è praticamente lo stesso:
function foo (a, b, c) {
return a + b + c;
}
foo(0, 1, 2); //3
foo.apply(null, [0, 1, 2]); //3
Ora, possiamo facilitare il processo di visualizzazione del applyfunzionamento semplicemente registrando la argumentsvariabile speciale:
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]
È facile dimostrare la mia affermazione nel penultimo esempio:
function ahaExclamationMark () {
console.log(arguments.length);
console.log(arguments.hasOwnProperty(0));
}
ahaExclamationMark.apply(null, Array(2)); //2, true
(sì, gioco di parole previsto). La key => valuemappatura potrebbe non esistere nell'array a cui siamo passati apply, ma esiste sicuramente nella argumentsvariabile. È la stessa ragione per cui funziona l'ultimo esempio: le chiavi non esistono sull'oggetto che passiamo, ma esistono arguments.
Perché? Diamo un'occhiata alla Sezione 15.3.4.3 , dove Function.prototype.applyè definita. Principalmente cose che non ci interessano, ma ecco la parte interessante:
- Sia len il risultato della chiamata al metodo interno [[Get]] di argArray con argomento "lunghezza".
Il che significa in pratica: argArray.length. La specifica procede quindi a fare un semplice forciclo sugli lengthoggetti, creando un listvalore corrispondente ( listè un voodoo interno, ma è fondamentalmente un array). In termini di codice molto, molto sciolto:
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);
};
Quindi tutto ciò di cui abbiamo bisogno per imitare un argArrayin questo caso è un oggetto con una lengthproprietà. E ora possiamo vedere perché i valori non sono definiti, ma le chiavi no, su arguments: Creiamo i key=>valuemapping.
Accidenti, quindi questo potrebbe non essere stato più breve della parte precedente. Ma alla fine ci sarà la torta, quindi sii paziente! Tuttavia, dopo la sezione seguente (che sarà breve, lo prometto) possiamo iniziare a dissezionare l'espressione. Nel caso in cui te ne fossi dimenticato, la domanda era: come funziona:
Array.apply(null, { length: 5 }).map(Number.call, Number);
Arraygestisce più argomentiCosì! Abbiamo visto cosa succede quando si passa un lengthargomento a Array, ma nell'espressione, passiamo diverse cose come argomenti (un array di 5 undefined, per essere esatti). La sezione 15.4.2.1 ci dice cosa fare. L'ultimo paragrafo è tutto ciò che conta per noi, ed è formulato in modo davvero strano, ma si riduce a:
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! Otteniamo una matrice di diversi valori non definiti e restituiamo una matrice di questi valori non definiti.
Infine, possiamo decifrare quanto segue:
Array.apply(null, { length: 5 })
Abbiamo visto che restituisce un array contenente 5 valori indefiniti, con chiavi tutte esistenti.
Ora, alla seconda parte dell'espressione:
[undefined, undefined, undefined, undefined, undefined].map(Number.call, Number)
Questa sarà la parte più semplice, non contorta, poiché non si basa molto su hack oscuri.
Numbertratta l'inputFare Number(something)( sezione 15.7.1 ) si converte somethingin un numero, e questo è tutto. Il modo in cui ciò è un po 'contorto, specialmente nei casi di stringhe, ma l'operazione è definita nella sezione 9.3 nel caso tu sia interessato.
Function.prototype.callcallè applyil fratello, definito nella sezione 15.3.4.4 . Invece di prendere una serie di argomenti, prende semplicemente gli argomenti che ha ricevuto e li trasmette.
Le cose diventano interessanti quando fai più di una catena callinsieme, fai girare lo strano fino a 11:
function log () {
console.log(this, arguments);
}
log.call.call(log, {a:4}, {a:5});
//{a:4}, [{a:5}]
//^---^ ^-----^
// this arguments
Questo è abbastanza degno fino a quando non capisci cosa sta succedendo. log.callè solo una funzione, equivalente a qualsiasi altro callmetodo di funzione , e come tale, ha anche un callmetodo su se stesso:
log.call === log.call.call; //true
log.call === Function.call; //true
E che cosa callfare? Accetta un thisArge un sacco di argomenti e chiama la sua funzione genitore. Possiamo definirlo tramite apply (di nuovo, codice molto sciolto, non funzionerà):
Function.prototype.call = function (thisArg) {
var args = arguments.slice(1); //I wish that'd work
return this.apply(thisArg, args);
};
Tracciamo come va:
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}])
.maptuttoNon è ancora finita. Vediamo cosa succede quando si fornisce una funzione alla maggior parte dei metodi di 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
Se non forniamo un thisargomento noi stessi, per impostazione predefinita window. Prendi nota dell'ordine in cui vengono forniti gli argomenti al nostro callback, e risaltiamo fino all'11:
arr.forEach(log.call, log);
//'a', [0, ['a', 'b', 'c']]
//'b', [1, ['a', 'b', 'c']]
//'b', [2, ['a', 'b', 'c']]
// ^ ^
Whoa whoa whoa ... facciamo un passo indietro. Cosa sta succedendo qui? Possiamo vedere nella sezione 15.4.4.18 , dove forEachè definito, accade praticamente quanto segue:
var callback = log.call,
thisArg = log;
for (var i = 0; i < arr.length; i += 1) {
callback.call(thisArg, arr[i], i, arr);
}
Quindi, otteniamo questo:
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);
Ora possiamo vedere come .map(Number.call, Number)funziona:
Number.call.call(Number, arr[i], i, arr);
Number.call(arr[i], i, arr);
Number(i, arr);
Che restituisce la trasformazione di i , l'indice corrente, a un numero.
L'espressione
Array.apply(null, { length: 5 }).map(Number.call, Number);
Funziona in due parti:
var arr = Array.apply(null, { length: 5 }); //1
arr.map(Number.call, Number); //2
La prima parte crea una matrice di 5 elementi non definiti. Il secondo va oltre quella matrice e prende i suoi indici, risultando in una matrice di indici di elementi:
[0, 1, 2, 3, 4]
ahaExclamationMark.apply(null, Array(2)); //2, true. Perché ritorna 2e truerispettivamente? Non stai passando solo un argomento, cioè Array(2)qui?
apply, ma quell'argomento viene "diviso" in due argomenti passati alla funzione. Puoi vederlo più facilmente nei primi applyesempi. Il primo console.logmostra quindi che effettivamente abbiamo ricevuto due argomenti (i due elementi dell'array) e il secondo console.logmostra che l'array ha una key=>valuemappatura nel primo slot (come spiegato nella prima parte della risposta).
log.apply(null, document.getElementsByTagName('script'));non è necessario per funzionare e non funziona in alcuni browser, e [].slice.call(NodeList)per trasformare un NodeList in un array non funzionerà neanche in essi.
thisdefault è solo Windowin modalità non rigorosa.
Disclaimer : Questa è una descrizione molto formale del codice di cui sopra - questo è come io so come spiegarlo. Per una risposta più semplice, controlla la grande risposta di Zirak sopra. Questa è una specifica più approfondita sul tuo viso e meno "aha".
Qui stanno accadendo molte cose. Rompiamolo un po '.
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
Nella prima riga, il costruttore di array viene chiamato come funzione con Function.prototype.apply.
thisvalore è nullche non ha importanza per il costruttore di array ( thisè lo stesso thisdel contesto in base al 15.3.4.3.2.a.new Arrayviene chiamato passaggio di un oggetto con una lengthproprietà - che fa sì che quell'oggetto sia un array come per tutto ciò che conta a .applycausa della seguente clausola in .apply:
.applyè il passaggio di argomenti tra 0 .length, perché richiamare [[Get]]su { length: 5 }con i valori da 0 a 4 Rese undefinedcostruttore matrice è chiamato con cinque argomenti cui valore èundefined (ottenendo una proprietà di un oggetto sommerso).var arr = Array.apply(null, { length: 5 });Crea quindi un elenco di cinque valori indefiniti.Nota : Notare la differenza qui tra Array.apply(0,{length: 5})e Array(5), il primo crea cinque volte il tipo di valore primitivo undefinede il secondo crea un array vuoto di lunghezza 5. In particolare, a causa del .mapcomportamento di (8.b) e in particolare [[HasProperty].
Quindi il codice sopra riportato in una specifica conforme è lo stesso di:
var arr = [undefined, undefined, undefined, undefined, undefined];
arr.map(Number.call, Number); // Calculate and return a number based on the index passed
Ora via alla seconda parte.
Array.prototype.mapchiama la funzione di callback (in questo caso Number.call) su ciascun elemento dell'array e usa il thisvalore specificato (in questo caso impostando il thisvalore su `Numero).Number.call) è l'indice e il primo è questo valore.Numberviene chiamato con thisas undefined(il valore dell'array) e l'indice come parametro. Quindi è fondamentalmente lo stesso di mappare ciascuno undefinedal suo indice di array (poiché la chiamata Numberesegue la conversione del tipo, in questo caso da numero a numero senza cambiare l'indice).Pertanto, il codice sopra riportato prende i cinque valori indefiniti e li associa ciascuno al suo indice nella matrice.
Ecco perché otteniamo il risultato nel nostro codice.
Array.apply(null,[2])è come quello Array(2)che crea una matrice vuota di lunghezza 2 e non una matrice contenente il valore primitivo undefineddue volte. Vedi la mia modifica più recente nella nota dopo la prima parte, fammi sapere se è abbastanza chiara e in caso contrario chiarirò questo.
{length: 2}finge un array con due elementi che il Arraycostruttore inserisce nell'array appena creato. Poiché non esiste un array reale che accede agli elementi non presenti, undefinedviene quindi inserito. Bel trucco :)
Come hai detto, la prima parte:
var arr = Array.apply(null, { length: 5 });
crea una matrice di 5 undefinedvalori.
La seconda parte chiama la mapfunzione dell'array che accetta 2 argomenti e restituisce un nuovo array della stessa dimensione.
Il primo argomento che mapprende è in realtà una funzione da applicare su ogni elemento dell'array, si prevede che sia una funzione che accetta 3 argomenti e restituisce un valore. Per esempio:
function foo(a,b,c){
...
return ...
}
se passiamo la funzione foo come primo argomento verrà chiamata per ogni elemento con
Il secondo argomento che mapprende viene passato alla funzione che passi come primo argomento. Ma non sarebbe a, b, né c in caso di foo, sarebbe this.
Due esempi:
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]
e un altro solo per renderlo più chiaro:
function qux(a,b,c){
return a
}
var newArr4 = arr2.map(qux,9);
//newArr4 is equal to [3,4,5]
E che dire di Number.call?
Number.call è una funzione che accetta 2 argomenti e tenta di analizzare il secondo argomento in un numero (non sono sicuro di cosa faccia con il primo argomento).
Poiché il secondo argomento che mappassa è l'indice, il valore che verrà inserito nella nuova matrice in quell'indice è uguale all'indice. Proprio come la funzione baznell'esempio sopra. Number.callproverà ad analizzare l'indice - restituirà naturalmente lo stesso valore.
Il secondo argomento che hai passato alla mapfunzione nel tuo codice non ha effettivamente un effetto sul risultato. Correggimi se sbaglio, per favore.
Number.callnon è una funzione speciale che analizza gli argomenti ai numeri. È giusto === Function.prototype.call. Solo il secondo argomento, la funzione che viene passata comethis -value a call, è rilevante - .map(eval.call, Number), .map(String.call, Number)e .map(Function.prototype.call, Number)sono tutti equivalenti.
Un array è semplicemente un oggetto che comprende il campo "lunghezza" e alcuni metodi (ad esempio push). Quindi arr in var arr = { length: 5}è sostanzialmente lo stesso di un array in cui i campi 0..4 hanno il valore predefinito che non è definito (cioè arr[0] === undefinedproduce true).
Per quanto riguarda la seconda parte, la mappa, come suggerisce il nome, mappa da un array a uno nuovo. Lo fa attraversando l'array originale e invocando la funzione di mappatura su ciascun elemento.
Non resta che convincerti che il risultato della funzione di mappatura è l'indice. Il trucco è usare il metodo chiamato 'call' (*) che invoca una funzione con la piccola eccezione che il primo parametro è impostato come contesto 'questo', e il secondo diventa il primo parametro (e così via). Per coincidenza, quando viene invocata la funzione di mappatura, il secondo parametro è l'indice.
Ultimo ma non meno importante, il metodo che viene invocato è il numero "Class", e come sappiamo in JS, una "classe" è semplicemente una funzione, e questo (numero) si aspetta che il primo parametro sia il valore.
(*) trovato nel prototipo di Function (e Number è una funzione).
Mashal
[undefined, undefined, undefined, …]e new Array(n)o {length: n}- questi ultimi sono radi , cioè non hanno elementi. Questo è molto rilevante per map, ed è per questo che è Array.applystato usato lo strano .
Array.apply(null, Array(30)).map(Number.call, Number)è più facile da leggere poiché evita di fingere che un oggetto semplice sia un array.