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.apply
gestisce gli argomentiArray
gestisce più argomentiNumber
funzione gestisce gli argomentiFunction.prototype.call
faSono 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 length
variabile magica , ma al suo centro, è una key => value
mappa 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.length
e il numero di key=>value
mappature dell'array, che può essere diverso da arr.length
.
L'espansione dell'array tramite arr.length
non crea nuovi key=>value
mapping, 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.map
non 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.toUpperCase
avrebbe 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 len
elementi sull'array, non creiamo i key => value
mapping, semplicemente modifichiamo la length
proprietà.
Ora che lo abbiamo rimosso, diamo un'occhiata alla seconda cosa magica:
Function.prototype.apply
funzionaQuello che apply
fa è 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 apply
funzionamento semplicemente registrando la arguments
variabile 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 => value
mappatura potrebbe non esistere nell'array a cui siamo passati apply
, ma esiste sicuramente nella arguments
variabile. È 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 for
ciclo sugli length
oggetti, creando un list
valore 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 argArray
in questo caso è un oggetto con una length
proprietà. E ora possiamo vedere perché i valori non sono definiti, ma le chiavi no, su arguments
: Creiamo i key=>value
mapping.
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);
Array
gestisce più argomentiCosì! Abbiamo visto cosa succede quando si passa un length
argomento 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.
Number
tratta l'inputFare Number(something)
( sezione 15.7.1 ) si converte something
in 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.call
call
è apply
il 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 call
insieme, 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 call
metodo di funzione , e come tale, ha anche un call
metodo su se stesso:
log.call === log.call.call; //true
log.call === Function.call; //true
E che cosa call
fare? Accetta un thisArg
e 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}])
.map
tuttoNon è 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 this
argomento 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 2
e true
rispettivamente? 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 apply
esempi. Il primo console.log
mostra quindi che effettivamente abbiamo ricevuto due argomenti (i due elementi dell'array) e il secondo console.log
mostra che l'array ha una key=>value
mappatura 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.
this
default è solo Window
in 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
.
this
valore è null
che non ha importanza per il costruttore di array ( this
è lo stesso this
del contesto in base al 15.3.4.3.2.a.new Array
viene chiamato passaggio di un oggetto con una length
proprietà - che fa sì che quell'oggetto sia un array come per tutto ciò che conta a .apply
causa 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 undefined
costruttore 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 undefined
e il secondo crea un array vuoto di lunghezza 5. In particolare, a causa del .map
comportamento 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.map
chiama la funzione di callback (in questo caso Number.call
) su ciascun elemento dell'array e usa il this
valore specificato (in questo caso impostando il this
valore su `Numero).Number.call
) è l'indice e il primo è questo valore.Number
viene chiamato con this
as undefined
(il valore dell'array) e l'indice come parametro. Quindi è fondamentalmente lo stesso di mappare ciascuno undefined
al suo indice di array (poiché la chiamata Number
esegue 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 undefined
due 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 Array
costruttore inserisce nell'array appena creato. Poiché non esiste un array reale che accede agli elementi non presenti, undefined
viene quindi inserito. Bel trucco :)
Come hai detto, la prima parte:
var arr = Array.apply(null, { length: 5 });
crea una matrice di 5 undefined
valori.
La seconda parte chiama la map
funzione dell'array che accetta 2 argomenti e restituisce un nuovo array della stessa dimensione.
Il primo argomento che map
prende è 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 map
prende 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 map
passa è l'indice, il valore che verrà inserito nella nuova matrice in quell'indice è uguale all'indice. Proprio come la funzione baz
nell'esempio sopra. Number.call
proverà ad analizzare l'indice - restituirà naturalmente lo stesso valore.
Il secondo argomento che hai passato alla map
funzione nel tuo codice non ha effettivamente un effetto sul risultato. Correggimi se sbaglio, per favore.
Number.call
non è 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] === undefined
produce 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.apply
stato 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.