Creazione di intervalli in JavaScript: strana sintassi


129

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?


2
L'IMO Array.apply(null, Array(30)).map(Number.call, Number)è più facile da leggere poiché evita di fingere che un oggetto semplice sia un array.
fncomp,

10
@fncomp Non utilizzare né per creare effettivamente un intervallo. Non solo è più lento dell'approccio diretto, ma non è altrettanto facile da capire. È difficile capire la sintassi (beh, davvero l'API e non la sintassi) qui che rende questa una domanda interessante ma un codice di produzione terribile IMO.
Benjamin Gruenbaum,

Sì, non suggerendo a nessuno di usarlo, ma ho pensato che fosse ancora più facile da leggere, rispetto alla versione letterale dell'oggetto.
fncomp,

1
Non sono sicuro del motivo per cui qualcuno vorrebbe farlo. Il tempo necessario per creare l'array in questo modo avrebbe potuto essere fatto in modo leggermente meno sexy ma molto più veloce: jsperf.com/basic-vs-extreme
Eric Hodonsky,

Risposte:


263

Comprendere questo "hack" richiede di capire diverse cose:

  1. Perché non lo facciamo e basta Array(5).map(...)
  2. Come Function.prototype.applygestisce gli argomenti
  3. Come Arraygestisce più argomenti
  4. Come la Numberfunzione gestisce gli argomenti
  5. Cosa Function.prototype.callfa

Sono argomenti piuttosto avanzati in javascript, quindi questo sarà più che piuttosto lungo. Inizieremo dall'alto. Allacciati!

1. Perché non solo 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:

2. Come Function.prototype.applyfunziona

Quello 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:

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

3. Come Arraygestisce più argomenti

Così! 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.

La prima parte dell'espressione

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.

4. Come Numbertratta l'input

Fare 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.

5. Giochi di Function.prototype.call

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

La parte successiva, o il .maptutto

Non è 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.

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

[0, 1, 2, 3, 4]

@Zirak Per favore aiutami a capire quanto segue ahaExclamationMark.apply(null, Array(2)); //2, true. Perché ritorna 2e truerispettivamente? Non stai passando solo un argomento, cioè Array(2)qui?
Geek,

4
@Geek Passiamo solo un argomento a 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).
Zirak,

4
A causa di (alcune) richieste , ora puoi goderti la versione audio: dl.dropboxusercontent.com/u/24522528/SO-answer.mp3
Zirak,

1
Nota che passare un NodeList, che è un oggetto host, a un metodo nativo come in 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.
RobG

2
Una correzione: il thisdefault è solo Windowin modalità non rigorosa.
ComFreek

21

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.

  • Il thisvalore è nullche non ha importanza per il costruttore di array ( thisè lo stesso thisdel contesto in base al 15.3.4.3.2.a.
  • Quindi 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:
    • Sia len il risultato della chiamata al metodo interno [[Get]] di argArray con argomento "lunghezza".
  • Come tale, .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).
  • 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.
  • 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).
  • Il secondo parametro del callback nella mappa (in questo caso Number.call) è l'indice e il primo è questo valore.
  • Ciò significa che 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.


1
Per i documenti: Specifiche su come funziona la mappa: es5.github.io/#x15.4.4.19 , Mozilla ha uno script di esempio che funziona secondo tale specifica su developer.mozilla.org/en-US/docs/Web/JavaScript/ Riferimento / ...
Patrick Evans,

1
Ma perché funziona solo con Array.apply(null, { length: 2 })e non con Array.apply(null, [2])ciò che chiamerebbe anche il Arraycostruttore passare 2come valore di lunghezza? violino
Andreas,

@Andreas 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.
Benjamin Gruenbaum,

Non ho capito come funziona alla prima esecuzione ... Dopo la seconda lettura ha senso. {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 :)
Andreas,

5

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

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

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.


1
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.
Bergi,

0

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


1
C'è un'enorme differenza tra [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 .
Bergi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.