Come usare Map, Filter e Reduce in JavaScript

Al giorno d'oggi la programmazione funzionale ha fatto un bel salto nel mondo dello sviluppo. E per una buona ragione: le tecniche funzionali possono aiutarti a scrivere più codice dichiarativo che è più facile da capire a colpo d'occhio, refactoring e test. 

Uno dei capisaldi della programmazione funzionale è l'uso speciale delle liste e delle operazioni di lista. E quelle cose sono esattamente ciò che il suono è come sono: matrici di cose e cose che fai a loro. Ma la mentalità funzionale li tratta un po 'diversamente da come ci si potrebbe aspettare.

Questo articolo darà un'occhiata da vicino a ciò che mi piace chiamare le operazioni "big three": carta geografica, filtro, e ridurre. Avvolgere la testa attorno a queste tre funzioni è un passo importante verso la possibilità di scrivere codice funzionale pulito e apre le porte alle tecniche di programmazione funzionale e reattiva estremamente potenti.

Significa anche che non dovrai mai scrivere a per loop di nuovo.

Curioso? Tuffiamoci dentro.

Una mappa da elenco a elenco

Spesso, ci troviamo a dover prendere un array e modificare ogni elemento in esso esattamente nello stesso modo. Esempi tipici di questo sono la quadratura di ogni elemento in una matrice di numeri, il recupero del nome da un elenco di utenti o l'esecuzione di un'espressione regolare contro una serie di stringhe.

carta geografica è un metodo costruito per fare esattamente questo. È definito su Array.prototype, quindi puoi chiamarlo su qualsiasi array e accetta un callback come primo argomento. 

Quando chiami carta geografica su un array, esegue tale callback su ogni elemento al suo interno, restituendo a nuovo array con tutti i valori restituiti dal callback.

Sotto il cappuccio, carta geografica passa tre argomenti alla tua richiamata:

  1. Il oggetto corrente nell'array
  2. Il indice dell'array dell'articolo corrente
  3. Il intero array hai chiamato la mappa 

Diamo un'occhiata ad un codice.

carta geografica in pratica

Supponiamo di avere un'app che mantiene una serie di compiti per il giorno. Ogni compito è un oggetto, ciascuno con a nome e durata proprietà:

// Le durate sono in minuti var tasks = ['name': 'Write for Envato Tuts +', 'duration': 120, 'name': 'Work out', 'duration': 60, 'name' : 'Procrastinate on Duolingo', 'duration': 240];

Diciamo che vogliamo creare un nuovo array con solo il nome di ogni attività, in modo da poter dare un'occhiata a tutto ciò che abbiamo fatto oggi. Usare un per loop, scriveremmo qualcosa del genere:

var task_names = []; per (var i = 0, max = tasks.length; i < max; i += 1)  task_names.push(tasks[i].name); 

JavaScript offre anche a per ciascuno ciclo continuo. Funziona come a per loop, ma gestisce tutta la confusione di controllare il nostro indice di loop rispetto alla lunghezza dell'array per noi:

var task_names = []; tasks.forEach (function (task) task_names.push (task.name););

utilizzando carta geografica, possiamo scrivere:

var task_names = tasks.map (function (task, index, array) return task.name;);

Includo il indice e  schieramento parametri per ricordarti che sono lì se ne hai bisogno. Dato che non li ho usati qui, però, potresti lasciarli fuori, e il codice funzionerebbe bene.

Ci sono alcune importanti differenze tra i due approcci:

  1. utilizzando carta geografica, non devi gestire lo stato di per loop te stesso.
  2. È possibile operare direttamente sull'elemento, piuttosto che dover indicizzare nell'array.
  3. Non è necessario creare un nuovo array e Spingere dentro. carta geografica restituisce il prodotto finito tutto in una volta, quindi possiamo semplicemente assegnare il valore di ritorno ad una nuova variabile.
  4. tu fare ricordarsi di includere a ritorno dichiarazione nel tuo callback. Se non lo fai, avrai un nuovo array pieno di non definito

Si scopre, tutti delle funzioni che vedremo oggi condividono queste caratteristiche.

Il fatto che non dobbiamo gestire manualmente lo stato del loop rende il nostro codice più semplice e più gestibile. Il fatto che possiamo operare direttamente sull'elemento invece di dover indicizzare nell'array rende le cose più leggibili. 

Usare un per ciascuno loop risolve entrambi questi problemi per noi. Ma carta geografica ha ancora almeno due distinti vantaggi:

  1. per ciascuno ritorna non definito, quindi non concatena con altri metodi di array. carta geografica restituisce un array, quindi tu può associarlo ad altri metodi di array.
  2. carta geografica ritornauna matrice con il prodotto finito, piuttosto che richiederci di mutare una matrice all'interno del ciclo. 

Mantenere il numero di luoghi in cui si modifica lo stato al minimo assoluto è un importante principio della programmazione funzionale. Rende il codice più sicuro e più comprensibile.

Ora è anche il momento giusto per far notare che se si è nel nodo, testando questi esempi nella console del browser Firefox o usando Babel o Traceur, è possibile scrivere questo in modo più conciso con le funzioni freccia ES6:

var task_names = tasks.map ((task) => task.name);

Le funzioni della freccia lasciano uscire il ritorno parola chiave in one-liner. 

Non diventa molto più leggibile di così.

gotchas

Il callback a cui si passa carta geografica deve avere un esplicito ritorno dichiarazione, o carta geografica sputerà un array pieno di non definito. Non è difficile ricordare di includere un ritorno valore, ma non è difficile da dimenticare. 

Se tu fare dimenticare, carta geografica non si lamenteranno Invece, restituirà tranquillamente un array pieno di nulla. Errori silenziosi come quello possono essere sorprendentemente difficili da debugare. 

Fortunatamente, questo è il solo Gotcha con carta geografica. Ma è una trappola abbastanza comune che sono obbligato a sottolineare: Assicurati sempre che la tua callback contenga un ritorno dichiarazione!

Implementazione

Leggere le implementazioni è una parte importante della comprensione. Quindi, scriviamo il nostro leggero carta geografica per capire meglio cosa sta succedendo sotto il cofano. Se vuoi vedere un'implementazione di qualità di produzione, controlla il polyfill di Mozilla su MDN.

var map = function (array, callback) var new_array = []; array.forEach (function (element, index, array) new_array.push (callback (elemento));); return new_array; ; var task_names = map (tasks, function (task) return task.name;);

Questo codice accetta un array e una funzione di callback come argomenti. Quindi crea una nuova matrice; esegue il callback su ogni elemento dell'array che abbiamo passato; spinge i risultati nel nuovo array; e restituisce il nuovo array. Se lo esegui nella tua console, otterrai lo stesso risultato di prima. Assicurati di aver inizializzato compiti prima di testarlo!

Mentre usiamo un ciclo for sotto il cofano, il wrapping in una funzione nasconde i dettagli e ci lascia lavorare invece con l'astrazione. 

Ciò rende il nostro codice più dichiarativo - dice che cosa fare, no Come per farlo. Apprezzerai quanto più leggibile, manutenibile e, erm, debuggable questo può rendere il tuo codice.

Filtra il rumore

Il prossimo delle nostre operazioni con gli array è filtro. Fa esattamente quello che sembra: prende un array e filtra elementi indesiderati.

Piace carta geografica, filtro è definito su Array.prototype. È disponibile su qualsiasi array e si passa a un callback come primo argomento. filtro esegue quella richiamata su ogni elemento dell'array e sputa a nuovo array contenente solo gli elementi per cui è stato restituito il callback vero.

Piace anche a carta geografica, filtro passa i tre argomenti di callback:

  1. Il oggetto corrente 
  2. Il indice corrente
  3. Il schieramento hai chiamato filtro sopra

filtro in pratica

Rivediamo il nostro esempio di attività. Invece di estrarre i nomi di ogni attività, diciamo che voglio ottenere un elenco delle attività che mi hanno richiesto due o più ore per essere completato. 

utilizzando per ciascuno, scriveremmo:

var difficult_tasks = []; tasks.forEach (function (task) if (task.duration> = 120) difficult_tasks.push (task););

Con filtro:

var difficult_tasks = tasks.filter (function (task) return task.duration> = 120;); // Utilizzo di ES6 var difficult_tasks = tasks.filter ((task) => task.duration> = 120);

Qui, sono andato avanti e ho lasciato fuori il indice e schieramento argomenti alla nostra callback, dal momento che non li usiamo.

Proprio come carta geografica, filtro ci permette di:

  • evitare di mutare una matrice all'interno di a per ciascuno o per ciclo continuo
  • assegna il suo risultato direttamente a una nuova variabile, piuttosto che inserire una matrice definita altrove

gotchas

Il callback a cui si passa carta geografica deve includere una dichiarazione di ritorno se vuoi che funzioni correttamente. Con filtro, devi anche includere una dichiarazione di reso, e tu dovere assicurati che restituisca un valore booleano.

Se si dimentica la dichiarazione di reso, la richiamata verrà restituita non definito, quale filtro costringerà a falso. Invece di lanciare un errore, restituisce automaticamente un array vuoto! 

Se vai sull'altra rotta e restituisci qualcosa che è non è esplicitamente vero o falso, poi filtro cercherà di capire cosa intendi applicando le regole di coercizione di JavaScript. Più spesso, questo è un bug. E, proprio come dimenticare la tua dichiarazione di ritorno, sarà silenziosa. 

Sempre assicurati che le tue callback includano un'istruzione di reso esplicita. E sempre assicurati di avere le tue callback in filtro ritorno vero o falso. La tua sanità mentale ti ringrazierà.

Implementazione

Ancora una volta, il modo migliore per capire un pezzo di codice è ... beh, per scriverlo. Facciamo rotolare il nostro peso leggero filtro. I bravi ragazzi di Mozilla hanno anche un polyfill per la forza industriale da leggere.

var filter = function (array, callback) var filtered_array = []; array.forEach (function (element, index, array) if (callback (elemento, indice, array)) filtered_array.push (elemento);); return filtered_array; ;

Ridurre gli array

carta geografica crea un nuovo array trasformando ogni elemento in un array, individualmente. filtro crea una nuova matrice rimuovendo gli elementi che non appartengono. ridurre, d'altra parte, prende tutti gli elementi in un array, e riduce loro in un unico valore.

Proprio come carta geograficafiltroridurre è definito su Array.prototype e così disponibile su qualsiasi array, e si passa un callback come primo argomento. Ma ci vuole anche un secondo argomento opzionale: il valore per iniziare a combinare tutti gli elementi dell'array in. 

ridurre passa i tuoi quattro argomenti di callback:

  1. Il valore corrente
  2. Il valore precedente
  3. Il indice corrente
  4. Il schieramento hai chiamato ridurre sopra

Si noti che la richiamata ottiene a valore precedente a ogni iterazione. Alla prima iterazione, lì è nessun valore precedente. Questo è il motivo per cui hai la possibilità di passare ridurre un valore iniziale: agisce come "valore precedente" per la prima iterazione, quando altrimenti non lo sarebbe.

Infine, tieni presente questo ridurre restituisce a valore singolo, non una matrice contenente un singolo oggetto. Questo è più importante di quanto possa sembrare, e tornerò ad esso negli esempi.

ridurre in pratica

Da ridurre è la funzione che le persone trovano più aliene all'inizio, inizieremo camminando passo dopo passo attraverso qualcosa di semplice.

Diciamo che vogliamo trovare la somma di un elenco di numeri. Usando un ciclo, che assomiglia a questo:

var numbers = [1, 2, 3, 4, 5], total = 0; numbers.forEach (funzione (numero) total + = numero;);

Anche se questo non è un cattivo uso per per ciascunoridurre ha ancora il vantaggio di permetterci di evitare la mutazione. Con ridurre, scriveremmo:

var total = [1, 2, 3, 4, 5] .reduce (funzione (precedente, corrente) return previous + current;, 0);

Per prima cosa, chiamiamo ridurre sulla nostra lista di numeri. Passiamo ad esso un callback, che accetta il valore precedente e il valore corrente come argomenti, e restituisce il risultato di aggiungerli insieme. Da quando siamo passati 0 come secondo argomento a ridurre, lo userà come valore di precedente alla prima iterazione.

Se lo facciamo passo dopo passo, assomiglia a questo:

Iterazione  Precedente attuale Totale
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10
5 10 5 15

Se non sei un fan delle tabelle, esegui questo snippet nella console:

var total = [1, 2, 3, 4, 5] .reduce (funzione (precedente, corrente, indice) var val = precedente + corrente; console.log ("Il valore precedente è" + precedente + "; il valore è "+ current +" e l'iterazione corrente è "+ (index + 1)); return val;, 0); console.log ("Il ciclo è terminato e il valore finale è" + total + ".");

Per ricapitolare: ridurre itera su tutti gli elementi di un array, combinandoli come specificato nel callback. Ad ogni iterazione, la richiamata ha accesso a precedente valore, qual è total-so-far, o valore accumulato; il valore corrente; il indice corrente; e il tutto schieramento, se ne hai bisogno.

Torniamo all'esempio dei nostri compiti. Abbiamo ottenuto un elenco di nomi di attività da carta geografica, e un elenco filtrato di compiti che ha richiesto molto tempo con ... beh, filtro

Cosa succederebbe se volessimo sapere il tempo totale che abbiamo trascorso lavorando oggi?

Usare un per ciascuno loop, dovresti scrivere:

var total_time = 0; tasks.forEach (function (task) // Il segno più costruisce solo // task.duration da una stringa a un numero total_time + = (+ task.duration););

Con ridurre, ciò diventa:

var total_time = tasks.reduce (function (precedente, current) return previous + current;, 0); // Utilizzo delle funzioni freccia var total_time = tasks.reduce ((precedente, corrente) precedente + corrente);

Facile. 

Questo è quasi tutto quello che c'è da fare. Quasi, perché JavaScript ci fornisce un altro metodo poco conosciuto, chiamato reduceRight. Negli esempi sopra, ridurre iniziato al primo elemento nell'array, iterando da sinistra a destra:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduce (function (precedente, current) return previous.concat (current);); console.log (concatenati); // [1, 2, 3, 4, 5, 6];

reduceRight fa la stessa cosa, ma nella direzione opposta:

var array_of_arrays = [[1, 2], [3, 4], [5, 6]]; var concatenated = array_of_arrays.reduceRight (funzione (precedente, corrente) return previous.concat (current);); console.log (concatenati); // [5, 6, 3, 4, 1, 2];

Io uso ridurre ogni giorno, ma non ho mai avuto bisogno reduceRight. Suppongo che probabilmente non lo farai neanche. Ma se mai lo fai, ora sai che è lì.

gotchas

I tre grandi trucchi con ridurre siamo:

  1. Dimenticare di ritorno
  2. Dimenticando un valore iniziale
  3. Aspettarsi un array quando ridurre restituisce un singolo valore

Fortunatamente, i primi due sono facili da evitare. Decidere quale dovrebbe essere il tuo valore iniziale dipende da quello che stai facendo, ma ne prenderai il controllo rapidamente.

L'ultimo potrebbe sembrare un po 'strano. Se ridurre restituisce sempre un solo valore, perché dovresti aspettarti un array?

Ci sono alcune buone ragioni per questo. Primo, ridurre restituisce sempre un singolo valore, non sempre un singolo numero. Se si riduce una matrice di matrici, ad esempio, verrà restituito un singolo array. Se hai l'abitudine o la riduzione degli array, sarebbe giusto aspettarsi che un array contenente un singolo oggetto non sia un caso speciale.

Secondo, se ridurre fatto restituire un array con un singolo valore, con cui naturalmente sarebbe piacevole carta geograficafiltro, e altre funzioni sugli array che probabilmente utilizzerai con esso. 

Implementazione

Tempo per il nostro ultimo sguardo sotto il cofano. Come al solito, Mozilla ha un polyfill antiproiettile da ridurre se si desidera verificarlo.

var reduce = function (array, callback, initial) var accumulator = initial || 0; array.forEach (function (element) accumulator = callback (accumulator, array [i]);); accumulatore di ritorno; ;

Due cose da notare, qui:

  1. Questa volta, ho usato il nome accumulatore invece di precedente. Questo è quello che vedrai di solito in natura.
  2. Io assegno accumulatore un valore iniziale, se un utente ne fornisce uno, e di default su 0, altrimenti. Questo è il vero ridurre si comporta anche.

Mettiamola insieme: mappa, filtro, riduzione e concatenabilità

A questo punto potresti non esserlo quello impressionato. 

Giusto: carta geografica, filtro, e ridurre, da soli, non sono terribilmente interessanti. 

Dopotutto, il loro vero potere risiede nella loro concatenabilità. 

Diciamo che voglio fare quanto segue:

  1. Colleziona due giorni di compiti.
  2. Converti le durate delle attività in ore anziché in minuti.
  3. Filtra tutto ciò che è durato due ore o più.
  4. Sommare tutto.
  5. Moltiplicare il risultato per una tariffa oraria per la fatturazione.
  6. Emette un importo in dollari formattato.

Per prima cosa, definiamo i nostri compiti per lunedì e martedì:

var monday = ['name': 'Write a tutorial', 'duration': 180, 'name': 'Some web development', 'duration': 120]; var martedì = ['nome': 'Continua a scrivere quel tutorial', 'durata': 240, 'nome': 'Un po' di sviluppo web ',' durata ': 180, ' nome ':' Un intero molto di niente ',' durata ': 240]; var tasks = [lunedi, martedì];

E ora, la nostra trasformazione adorabile:

 var result = tasks.reduce (function (accumulator, current) return accumulator.concat (current);). map (function (task) return (task.duration / 60);). filter (funzione (durata) return duration> = 2;). map (function (duration) return duration * 25;). reduce (function (accumulator, current) return [(+ accumulator) + (+ current)];). map (function (dollar_amount) return '$' + dollar_amount.toFixed (2);). reduce (function (formatted_dollar_amount) return formatted_dollar_amount;);

O, più concisamente:

 // Concatena il nostro array 2D in una singola lista var result = tasks.reduce ((acc, current) => acc.concat (current)) // Estrai la durata dell'attività e converti i minuti in ore .map ((task) = > task.duration / 60) // Filtra tutte le attività che hanno richiesto meno di due ore. filtro ((durata) => durata> = 2) // Moltiplica la durata di ciascuna attività in base alla nostra tariffa oraria .map ((durata) = > duration * 25) // Combina le somme in un importo in dollari .reduce ((acc, current) => [(+ acc) + (+ current)]) // Converti in un importo in dollari "piuttosto stampato". map ((amount) => '$' + amount.toFixed (2)) // Estrai l'unico elemento dell'array ottenuto da map .reduce ((formatted_amount) => formatted_amount); 

Se sei arrivato così lontano, questo dovrebbe essere piuttosto semplice. Ci sono due bit di stranezza da spiegare, però. 

Per prima cosa, sulla linea 10, devo scrivere:

// Remoinder omitted reduce (function (accumulator, current) return [(+ accumulator) + (+ current_];)

Due cose da spiegare qui:

  1. I più segni di fronte accumulatoreattuale costringere i loro valori ai numeri. Se non lo fai, il valore restituito sarà la stringa piuttosto inutile, "12510075100".
  2. Se non avvolgere quella somma tra parentesi, ridurre sputerà un singolo valore, non un array. Ciò finirebbe per gettare a TypeError, perché puoi usare solo carta geografica su un array! 

Il secondo bit che potrebbe renderti un po 'a disagio è l'ultimo ridurre, vale a dire:

// Remainder omitted map (function (dollar_amount) return '$' + dollar_amount.toFixed (2);). Reduce (function (formatted_dollar_amount) return formatted_dollar_amount;);

Quella chiamata a carta geografica restituisce un array contenente un singolo valore. Qui, chiamiamo ridurre per estrarre quel valore.

L'altro modo per farlo sarebbe quello di rimuovere la chiamata per ridurre e indicizzare nell'array carta geografica sputa fuori:

var result = tasks.reduce (function (accumulator, current) return accumulator.concat (current);). map (function (task) return (task.duration / 60);). filter (funzione (durata) return duration> = 2;). map (function (duration) return duration * 25;). reduce (function (accumulator, current) return [(+ accumulator) + (+ current)];). map (function (dollar_amount) return '$' + dollar_amount.toFixed (2);) [0];

Questo è perfettamente corretto. Se ti senti più a tuo agio nell'uso di un indice di array, vai avanti.

Ma ti incoraggio a non farlo. Uno dei modi più potenti per utilizzare queste funzioni è nel campo della programmazione reattiva, in cui non sarai libero di utilizzare gli indici di array. Dare il calcio a quell'abitudine ora renderà le tecniche di apprendimento reattivo molto più semplici.

Infine, vediamo come il nostro amico il per ciascuno loop lo farebbe:

var concatenated = lunedi.concat (martedì), fees = [], formatted_sum, hourly_rate = 25, total_fee = 0; concatenated.forEach (function (task) var duration = task.duration / 60; if (duration> = 2) fees.push (duration * hourly_rate);); fees.forEach (funzione (a pagamento) total_fee + = fee); var formatted_sum = '$' + total_fee.toFixed (2);

Tollerabile, ma rumoroso.

Conclusione e prossimi passi

In questo tutorial, hai imparato come carta geograficafiltro, e ridurre lavoro; come usarli; e approssimativamente come sono implementati. Hai visto che tutti ti permettono di evitare lo stato di mutamento, che usa perper ciascuno richiede loop e ora dovresti avere una buona idea di come concatenarli tutti insieme. 

Ormai sono sicuro che sei desideroso di pratica e ulteriore lettura. Ecco i miei primi tre suggerimenti su dove andare avanti:

  1. La superba serie di esercizi di Jafar Husain sulla programmazione funzionale in JavaScript, completa di una solida introduzione a Rx.js
  2. Corso Envato Tuts + Instructor Jason Rhodes su Programmazione funzionale in JavaScript
  3. La Guida per lo più alla programmazione funzionale, che approfondisce il motivo per cui evitiamo la mutazione e il pensiero funzionale in generale

JavaScript è diventato una delle lingue di fatto di lavorare sul web. Non è senza le sue curve di apprendimento, e ci sono un sacco di quadri e librerie per tenerti occupato, pure. Se stai cercando ulteriori risorse da studiare o da utilizzare nel tuo lavoro, dai un'occhiata a ciò che abbiamo a disposizione sul mercato Envato.

Se vuoi saperne di più su questo genere di cose, controlla il mio profilo di volta in volta; prendimi su Twitter (@PelekeS); o colpisci il mio blog su http://peleke.me.

Domande, commenti o confusioni? Lasciali sotto, e farò del mio meglio per tornare a ciascuno individualmente.

Scopri JavaScript: la guida completa

Abbiamo creato una guida completa per aiutarti a imparare JavaScript, sia che tu stia appena iniziando come sviluppatore web o che desideri esplorare argomenti più avanzati.