Creazione intervallo in JavaScript – syntax strana

Ho letto 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?

Comprendere questo “hack” richiede la comprensione di diverse cose:

  1. Perché non facciamo solo Array(5).map(...)
  2. Come Function.prototype.apply gestisce gli argomenti
  3. In che modo Array gestisce più argomenti
  4. Come la funzione Number gestisce gli argomenti
  5. Cosa Function.prototype.call fa

Sono argomenti piuttosto avanzati in javascript, quindi sarà molto più che lungo. Inizieremo dall’inizio. Allacciati!

1. Perché non solo Array(5).map ?

Che cos’è un array, davvero? Un object regolare, contenente le chiavi integer, che si associano ai valori. Ha altre caratteristiche speciali, ad esempio la variabile di length magica, ma al suo centro, è una key => value normale key => value mappa dei key => value , proprio come qualsiasi altro object. Giochiamo un po ‘con gli array, 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'] 

Otteniamo la differenza intrinseca tra il numero di elementi nell’array, arr.length e il numero di key=>value mappature del key=>value dell’array, che può essere diverso da arr.length .

L’espansione dell’array via arr.length non crea alcun nuovo key=>value mapping dei key=>value , quindi non è che l’array abbia valori indefiniti, non ha queste chiavi . E cosa succede quando cerchi di accedere a una proprietà inesistente? Sei undefined .

Ora possiamo alzare un po ‘la testa e vedere perché funzioni come arr.map non camminano su queste proprietà. Se arr[3] era semplicemente indefinito, e la chiave esisteva, tutte queste funzioni di array lo avrebbero semplicemente passato 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 utilizzato una chiamata al metodo per dimostrare ulteriormente che la chiave non era mai stata lì: chiamare undefined.toUpperCase avrebbe generato un errore, ma non lo fece. 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 punto: come Array(N) fa le cose. La sezione 15.4.2.2 descrive il processo. C’è un sacco di mumbo jumbo di cui non ci interessa, ma se riesci a leggere tra le righe (o puoi semplicemente 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 sotto l’assunto (che è verificato nelle specifiche attuali) che len è un uint32 valido, e non solo un numero qualsiasi di valori)

Così ora puoi capire perché fare Array(5).map(...) non funzionerebbe – non definiamo elementi di len sull’array, non creiamo key => value mappature di key => value , semplicemente alteriamo proprietà di length .

Ora che lo abbiamo tolto di mezzo, diamo un’occhiata alla seconda cosa magica:

2. Come Function.prototype.apply

Ciò che si apply è fondamentalmente prendere una matrice e srotolarla come argomenti 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 vedere come funziona apply semplicemente registrando la variabile speciale degli arguments :

 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). La key => value mapping dei key => value potrebbe non essere esistita nella matrice passata per apply , ma certamente esiste nella variabile arguments . È la stessa ragione per cui funziona l’ultimo esempio: le chiavi non esistono sull’object che passiamo, ma esistono negli arguments .

Perché? Diamo un’occhiata alla Sezione 15.3.4.3 , dove è definito Function.prototype.apply . Principalmente cose a cui non importa, ma ecco la parte interessante:

  1. Lascia che len sia il risultato della chiamata al metodo interno [[Get]] di argArray con argomento “length”.

Che in pratica significa: argArray.length . La specifica procede quindi a creare un semplice ciclo per oggetti di length superiore, creando un list di valori corrispondenti (l’ list è un voodoo interno, ma in pratica è 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 object con una proprietà length . E ora possiamo vedere perché i valori non sono definiti, ma le chiavi non lo sono, sugli arguments : creiamo la key=>value mappature dei key=>value .

Phew, quindi questo potrebbe non essere stato più breve della parte precedente. Ma alla fine ci sarà la torta, quindi sii paziente! Tuttavia, dopo la seguente sezione (che sarà breve, lo prometto), possiamo iniziare a dissezionare l'espressione. Nel caso lo avessi dimenticato, la domanda era: come funziona il seguente lavoro:

 Array.apply(null, { length: 5 }).map(Number.call, Number); 

3. Come Array gestisce più argomenti

Così! Abbiamo visto cosa succede quando passi un argomento di length ad Array , ma nell'espressione, passiamo diverse cose come argomenti (una matrice di 5 undefined , per essere precisi). La sezione 15.4.2.1 ci dice cosa fare. L'ultimo paragrafo è tutto ciò che conta per noi, ed è formulato in modo molto strano, ma in qualche modo 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 un array di diversi valori non definiti e restituiamo un array di questi valori non definiti.

La prima parte dell'espressione

Infine, possiamo decifrare quanto segue:

 Array.apply(null, { length: 5 }) 

Abbiamo visto che restituisce un array contenente 5 valori non definiti, con tutti i tasti esistenti.

Ora, alla seconda parte dell'espressione:

 [undefined, undefined, undefined, undefined, undefined].map(Number.call, Number) 

Questa sarà la parte più semplice e non contorta, poiché non si basa tanto su oscuri hack.

4. Come il Number considera l'input

Fare Number(something) ( sezione 15.7.1 ) converte something in un numero, e questo è tutto. In che modo ciò è un po 'complicato, specialmente nei casi di stringhe, ma l'operazione è definita nella sezione 9.3 nel caso siate interessati.

5. Giochi di Function.prototype.call

call is apply 's brother, definito nella sezione 15.3.4.4 . Invece di prendere una serie di argomenti, prende solo gli argomenti che ha ricevuto e li inoltra.

Le cose si fanno interessanti quando si concatenano più di una call , manovrando il bizzarro 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 finché non comprendi cosa sta succedendo. log.call è solo una funzione, equivalente al metodo di call di qualsiasi altra funzione, e come tale ha anche un metodo di call su se stesso:

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

E come si call ? Accetta un thisArg e un mucchio di argomenti e chiama la sua funzione genitore. Possiamo definirlo tramite apply (ancora, codice molto loose, non funzionerà):

 Function.prototype.call = function (thisArg) { var args = arguments.slice(1); //I wish that'd work return this.apply(thisArg, args); }; 

Rintracciamo come questo va giù:

 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}]) 

La parte successiva o la .map di tutto questo

Non è ancora finita. Vediamo cosa succede quando fornisci 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 personalmente this argomento, per impostazione predefinita viene visualizzata una window . Prendi nota dell'ordine in cui vengono forniti gli argomenti alla nostra richiamata e torniamo indietro fino a 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, il seguente succede molto spesso:

 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 funziona .map(Number.call, Number) :

 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, in un numero.

In conclusione,

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 ripassa quella matrice e prende i suoi indici, risultando in una matrice di indici di elementi:

 [0, 1, 2, 3, 4] 

Disclaimer : Questa è una descrizione molto formale del codice di cui sopra – questo è il modo in cui so come spiegarlo. Per una risposta più semplice – controlla l’ottima risposta di Zirak qui sopra. Questa è una specifica più approfondita in faccia e meno “aha”.


Molte cose stanno accadendo qui. Let’s rompere 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 Array ( this è lo stesso nel contesto secondo 15.3.4.3.2.a.
  • Quindi il new Array è chiamato essere passato un object con una proprietà length – che fa sì che quell’object sia una matrice come per tutto ciò che conta .apply causa della seguente clausola in .apply :
    • Lascia che len sia il risultato della chiamata al metodo interno [[Get]] di argArray con argomento “length”.
  • In quanto tale, .apply passa gli argomenti da 0 a .length , dal momento che chiamare [[Get]] su { length: 5 } con i valori da 0 a 4 produce undefined il costruttore di array viene chiamato con cinque argomenti il ​​cui valore undefined è undefined (ottenendo un proprietà non dichiarata di un object).
  • Il costruttore di array viene chiamato con 0, 2 o più argomenti . La proprietà length dell’array appena costruito è impostata sul numero di argomenti in base alla specifica e i valori sugli stessi valori.
  • Quindi var arr = Array.apply(null, { length: 5 }); crea un elenco di cinque valori non definiti.

Nota : notare la differenza tra Array.apply(0,{length: 5}) e Array(5) , il primo che crea cinque volte il tipo di valore primitivo undefined e il secondo che crea una matrice vuota di lunghezza 5. Specificamente, a causa di Comportamento di .map (8.b) e in particolare [[HasProperty] .

Quindi il codice sopra in una specifica conforms è 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 ogni elemento dell’array e utilizza il valore specificato (in questo caso l’impostazione di this valore su `Number).
  • Il secondo parametro del callback nella mappa (in questo caso Number.call ) è l’indice e il primo è il valore.
  • Ciò significa che Number viene chiamato con this come undefined (il valore dell’array) e l’indice come parametro. Quindi è fondamentalmente come mappare ogni undefined al suo indice di matrice (dato che chiamare Number esegue la conversione del tipo, in questo caso da numero a numero che non cambia l’indice).

Quindi, il codice sopra prende i cinque valori non definiti e mappa ciascuno al suo indice nella matrice.

Ecco perché otteniamo il risultato nel nostro codice.

Come hai detto tu, la prima parte:

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

crea una matrice di 5 valori undefined .

La seconda parte chiama la funzione map dell’array che accetta 2 argomenti e restituisce una nuova matrice della stessa dimensione.

Il primo argomento che assume la map è in realtà una funzione da applicare a ciascun elemento dell’array, è previsto 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à chiamato per ogni elemento con

  • a come valore dell’elemento corrente iterato
  • b come indice dell’elemento iterato corrente
  • c come l’intero array originale

Il secondo argomento che assume la map viene passato alla funzione che si passa 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] 

Quindi, per quanto riguarda Number.call?

Number.call è una funzione che accetta 2 argomenti e tenta di analizzare il secondo argomento in un numero (non sono sicuro di ciò che fa con il primo argomento).

Poiché il secondo argomento che la map sta passando è l’indice, il valore che verrà inserito nel nuovo array 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 funzione map nel tuo codice in realtà non ha effetto sul risultato. Correggimi se sbaglio, per favore.

Un array è semplicemente un object che comprende il campo “length” e alcuni metodi (ad es. Push). Quindi arr in var arr = { length: 5} è fondamentalmente la stessa di un array in cui i campi 0..4 hanno il valore predefinito che non è definito (per esempio arr[0] === undefined yields true).
Per quanto riguarda la seconda parte, la mappa, come suggerisce il nome, mappa da una matrice a una nuova. Lo fa attraversando l’array originale e invocando la funzione di mapping su ciascun elemento.

Tutto ciò che rimane è convincerti che il risultato della funzione di mapping è l’indice. Il trucco è usare il metodo chiamato ‘call’ (*) che richiama una funzione con la piccola eccezione che il primo parametro è impostato per essere il contesto ‘this’, e il secondo diventa il primo parametro (e così via). Per coincidenza, quando viene richiamata la funzione di mapping, il secondo parametro è l’indice.

Ultimo ma non meno importante, il metodo invocato è il numero “Class”, e come sappiamo in JS, una “Class” è semplicemente una funzione, e questa (Number) si aspetta che il primo parametro sia il valore.

(*) trovato nel prototipo di Function (e Number è una funzione).

Mashal