Metodi per Scorrere gli Array in JavaScript

Nelle lezioni precedenti ci siamo concentrati sulla sintassi JavaScript di base per lavorare con gli array.

In generale, tuttavia, sono i metodi definiti dalla classe Array ad essere i più potenti.

Mentre analizziamo questi metodi, bisogna tener presente che alcuni di essi modificano l'array su cui vengono chiamati e alcuni di essi lasciano l'array invariato. Un certo numero di metodi restituisce un array: a volte, questo è un nuovo array, e l'originale rimane invariato. Altre volte, un metodo modificherà l'array sul posto e restituirà anche un riferimento all'array modificato.

In questa lezione ci concentriamo sui metodi per iterare un array che, tipicamente, prendono in ingresso una funzione che viene applicata su ciascun elemento.

Concetti Chiave
  • I metodi di iterazione degli array in JavaScript, come forEach, map, filter, find, every, some, reduce e reduceRight, offrono modi potenti per lavorare con gli array.
  • Questi metodi accettano una funzione come primo argomento e la invocano per ogni elemento dell'array, passando il valore, l'indice e l'array stesso come argomenti.
  • I metodi forEach, map, filter, find, every, some, reduce e reduceRight hanno comportamenti specifici e restituiscono risultati diversi, come la modifica dell'array originale o la creazione di un nuovo array.
  • È importante comprendere le differenze tra questi metodi per utilizzarli in modo efficace.

Metodi di Iterazione per gli Array

I metodi descritti in questa lezione iterano sugli array passando gli elementi dell'array, in ordine, a una funzione fornita in ingresso, e forniscono modi convenienti per iterare, mappare, filtrare, testare e ridurre gli array.

Prima di spiegare i metodi in dettaglio, tuttavia, vale la pena fare alcune generalizzazioni su di essi:

  1. Primo, tutti questi metodi accettano una funzione come primo argomento e invocano quella funzione una volta per ogni elemento (o alcuni elementi) dell'array.

    Se l'array è sparso, la funzione che passiamo non viene invocata per elementi inesistenti. Nella maggior parte dei casi, la funzione che forniamo viene invocata con tre argomenti:

    1. il valore dell'elemento dell'array;
    2. l'indice dell'elemento dell'array;
    3. l'array stesso.

    Spesso, necessitiamo solo del primo di questi valori di argomento e possiamo ignorare il secondo e il terzo valore.

  2. La maggior parte dei metodi iteratori descritti in questa lezione accettano un secondo argomento opzionale.

    Se specificato, la funzione viene invocata come se fosse un metodo di questo secondo argomento. Cioè, il secondo argomento che passiamo diventa il valore della parola chiave this dentro la funzione che passiamo come primo argomento. Il valore di ritorno della funzione che passiamo è solitamente importante, ma metodi diversi gestiscono il valore di ritorno in modi diversi.

    Nessuno dei metodi descritti qui modifica l'array su cui vengono invocati (anche se la funzione che passiamo può modificare l'array, ovviamente).

Ognuna di queste funzioni viene invocata con una funzione come primo argomento, ed è molto comune definire quella funzione inline come parte dell'espressione di invocazione del metodo invece di usare una funzione esistente che è definita altrove. La sintassi delle funzioni anonime (o arrow function) funziona particolarmente bene con questi metodi, e la useremo negli esempi che seguono.

forEach

Il metodo forEach() itera attraverso un array, invocando una funzione specificata come primo argomento per ogni elemento.

Come abbiamo descritto, bisogna passare la funzione come primo argomento a forEach().

forEach() poi invoca la funzione con tre argomenti: il valore dell'elemento dell'array, l'indice dell'elemento dell'array, e l'array stesso.

Se si è interessati solo al valore dell'elemento dell'array, si può scrivere una funzione con solo un parametro e gli argomenti aggiuntivi saranno ignorati.

Analizziamo un esempio in cui, attraverso forEach, calcoliamo la somma di tutti gli elementi di un array numerico:

let dati = [1,2,3,4,5]
let somma = 0;

// Calcola la somma degli elementi dell'array
dati.forEach(valore => { somma += valore; });

console.log(somma);  // => 15

Il metodo forEach lavora sull'array di partenza e, attraverso la funzione che gli passiamo, è in grado di modificare l'array stesso. Per esempio, possiamo usare forEach per incrementare ogni elemento dell'array:

let dati = [1,2,3,4,5];

// Ora incrementa ogni elemento dell'array
dati.forEach(function(v, i, a) { a[i] = v + 1; });

// Adesso l'array è [2,3,4,5,6]
console.log(dati);  // => [2,3,4,5,6]

Si noti che forEach() non fornisce un modo per terminare l'iterazione prima che tutti gli elementi siano stati passati alla funzione. Cioè, non c'è equivalente dell'istruzione break che, invece, può essere usato con un normale ciclo for.

map

Il metodo map() effettua tre operazioni:

  1. Prende in ingresso una funzione come primo argomento;
  2. Applica questa funzione ad ogni elemento dell'array;
  3. Restituisce un nuovo array che contiene i valori restituiti dalla funzione applicata a ciascun elemento dell'array originale.

Consideriamo un esempio in cui vogliamo calcolare il quadrato di ogni numero in un array:

// Array di partenza
let a = [1, 2, 3, 4, 5];

// Applichiamo map per calcolare il quadrato di ogni numero
let b = a.map(x => x*x);

console.log(b);  // => [1, 4, 9, 16, 25]

La funzione che si passa a map() è invocata nello stesso modo di una funzione passata a forEach(). Per il metodo map(), tuttavia, la funzione che si passa deve restituire un valore.

Si noti che map() restituisce un nuovo array: non modifica l'array su cui è invocato. Se quell'array è sparso, la funzione non sarà chiamata per gli elementi mancanti, ma l'array restituito sarà sparso nello stesso modo dell'array originale: avrà la stessa lunghezza e gli stessi elementi mancanti.

Ad esempio, riprendendo il programma di prima che calcola i quadrati di un array, supponendo di applicare map() a un array sparso:

let sparso = [1, , 3, 4, , 6];

// Applichiamo map a sparso
let risultato = sparso.map(x => x * x);

console.log(risultato);  // => [1, , 9, 16, , 36]

Il nuovo array risultato ha la stessa lunghezza di sparso, e manca degli elementi in corrispondenza degli indici 1 e 4, proprio come l'array di partenza.

filter

Il metodo filter() restituisce un array contenente un sottoinsieme degli elementi dell'array su cui viene invocato.

Tale funzione richiede in ingresso un particolare tipo di funzione che, in gergo tecnico, prende il nome di predicato. Un predicato è una funzione che prende in ingresso un valore e restituisce true o false. Il metodo filter() applica il predicato a ogni elemento dell'array e restituisce un nuovo array contenente solo gli elementi per i quali il predicato ha restituito true.

Per chiarire il tutto, consideriamo un esempio in cui vogliamo filtrare un array di numeri per ottenere solo i numeri pari:

let a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Creiamo una funzione predicato che verifica se un numero è pari
let isEven = x => x % 2 === 0;

let b = a.filter(isEven);

console.log(b);  // => [2, 4, 6, 8, 10]

Si noti che filter() salta gli elementi mancanti negli array sparsi e che il suo valore di ritorno è sempre denso.

Un'interessante applicazione di filter() è quella di rimuovere gli elementi undefined da un array sparso. Per esempio, se abbiamo un array sparso e vogliamo ottenere un array denso con solo i valori definiti, possiamo usare filter() in questo modo:

let sparso = [1, , 3, , 5, , 7];

let denso = sparso.filter(x => x !== undefined && x !== null);

In questo caso abbiamo passato come predicato una funzione che restituisce sempre true. Ma poiché filter salta i valori undefined, il risultato sarà un array denso con solo i valori definiti:

console.log(denso);  // => [1, 3, 5, 7]

Analogamente, possiamo usare filter() per rimuovere gli elementi null da un array sparso:

let denso = sparso.filter(x => x !== undefined && x !== null);

find e findIndex

I metodi find e findIndex sono simili a filter in quanto iterano attraverso l'array cercando elementi per i quali la funzione predicato restituisce un valore true.

A differenza di filter(), tuttavia, questi due metodi smettono di iterare la prima volta che il predicato trova un elemento. Quando ciò accade, find() restituisce l'elemento corrispondente, e findIndex() restituisce l'indice dell'elemento corrispondente. Se non viene trovato alcun elemento corrispondente, find() restituisce undefined e findIndex() restituisce -1:

Ad esempio:

// Array di partenza
let a = [1, 2, 3, 4, 5];

// Trova l'indice del primo elemento pari a 3
let indice_3 = a.findIndex(x => x === 3);
console.log(indice_3);  // => 2: l'elemento 3 si trova all'indice 2

// Trova l'indice del primo elemento minore di 0
let indice_negativo = a.findIndex(x => x < 0);
console.log(indice_negativo);  // => -1; nessun numero negativo nell'array

// Trova il primo elemento multiplo di 5
let primo_multiplo_5 = a.find(x => x % 5 === 0);
console.log(primo_multiplo_5);  // => 5: questo è un multiplo di 5

// Trova il primo elemento multiplo di 7
let primo_multiplo_7 = a.find(x => x % 7 === 0);
console.log(primo_multiplo_7);  // => undefined: nessun multiplo di 7 nell'array

every e some

I metodi every e some sono predicati per array: applicano una funzione predicato passata come argomento agli elementi dell'array, quindi restituiscono true o false.

Il metodo every è come il quantificatore matematico "per tutti" \forall: restituisce true se e solo se la funzione predicato restituisce true per tutti gli elementi nell'array:

let a = [1,2,3,4,5];

// Verifica se tutti i valori sono minori di 10
let minori_di_10 = a.every(x => x < 10);
console.log(minori_di_10); // => true: tutti i valori sono minori di 10

// Verifica se tutti i valori sono pari
let tutti_pari = a.every(x => x % 2 === 0);
console.log(tutti_pari); // => false: non tutti i valori sono pari.

Il metodo some() è come il quantificatore matematico "esiste" \exists: restituisce true se esiste almeno un elemento nell'array per il quale il predicato restituisce true e restituisce false se e solo se il predicato restituisce false per tutti gli elementi dell'array:

let a = [1,2,3,4,5];

// Verifica se almeno un valore è pari
let almeno_un_pari = a.some(x => x % 2 === 0);
console.log(almeno_un_pari); // => true: ci sono numeri pari nell'array

// Verifica se almeno un valore è minore di 0
let almeno_un_negativo = a.some(x => x < 0);
console.log(almeno_un_negativo); // => false: non ci sono numeri negativi nell'array

Si noti che sia every() che some() smettono di iterare gli elementi dell'array non appena sanno quale valore restituire. some() restituisce true la prima volta che il predicato restituisce true e itera attraverso l'intero array solo se il predicato restituisce sempre false.

every() è l'opposto: restituisce false la prima volta che il vostro predicato restituisce false e itera tutti gli elementi solo se il vostro predicato restituisce sempre true. Si noti anche che, per convenzione matematica, every() restituisce true e some restituisce false quando invocati su un array vuoto.

reduce e reduceRight

I metodi reduce() e reduceRight() combinano gli elementi di un array, utilizzando la funzione specificata, per produrre un singolo valore.

Questa è un'operazione comune nella programmazione funzionale e viene anche chiamata inject e fold.

Gli esempi aiutano a illustrare come funziona. Supponiamo di avere un array così composto:

let a = [10, 20, 30, 40, 50];

Possiamo calcolare la somma di tutti gli elementi dell'array usando reduce() in questo modo:

let somma = a.reduce((acc, val) => acc + val, 0);
console.log(somma);  // => 150

Oppure, possiamo calcolare il prodotto di tutti gli elementi dell'array:

let prodotto = a.reduce((acc, val) => acc * val, 1);
console.log(prodotto);  // => 1200000

Un altro esempio comune è trovare il valore massimo in un array:

let massimo = a.reduce((acc, val) => acc > val ? acc : val);
console.log(massimo);  // => 50

reduce() accetta due argomenti. Il primo è la funzione che esegue l'operazione di riduzione. Il compito di questa funzione di riduzione è di combinare o ridurre in qualche modo due valori in un singolo valore e restituire quel valore ridotto. Negli esempi che abbiamo mostrato qui, le funzioni combinano due valori aggiungendoli, moltiplicandoli e scegliendo il più grande. Il secondo argomento (opzionale) è un valore iniziale da passare alla funzione.

Le funzioni utilizzate con reduce() sono diverse dalle funzioni utilizzate con forEach() e map(). I familiari valori valore, indice e array vengono passati come secondo, terzo e quarto argomento. Il primo argomento è il risultato accumulato della riduzione fino a quel momento. Alla prima chiamata della funzione, questo primo argomento è il valore iniziale che abbiamo passato come secondo argomento a reduce(). Nelle chiamate successive, è il valore restituito dalla precedente invocazione della funzione. Nel primo esempio, la funzione di riduzione viene chiamata per la prima volta con gli argomenti 0 e 10. Li aggiunge e restituisce 10. Viene poi chiamata di nuovo con gli argomenti 10 e 20 e restituisce 30. Successivamente, calcola 30+30=60, poi 60+40=100, e infine 100+50=150. Questo valore finale, 150, diventa il valore di ritorno di reduce().

Si noti che la terza chiamata a reduce() negli esempi ha solo un singolo argomento: non è specificato alcun valore iniziale. Quando invochiamo reduce() così senza valore iniziale, utilizza il primo elemento dell'array come valore iniziale. Questo significa che la prima chiamata alla funzione di riduzione avrà il primo e secondo elemento dell'array come primo e secondo argomento. Negli esempi di somma e prodotto, avremmo potuto omettere l'argomento del valore iniziale.

Chiamare reduce() su un array vuoto senza argomento di valore iniziale causa un TypeError. Se viene invocato con solo un valore, sia un array con un elemento e nessun valore iniziale o un array vuoto e un valore iniziale, restituisce semplicemente quel singolo valore senza mai chiamare la funzione di riduzione.

reduceRight() funziona proprio come reduce(), eccetto che elabora l'array dall'indice più alto al più basso (da destra a sinistra), piuttosto che dal più basso al più alto. Si potrebbe voler utilizzare reduceRight se l'operazione di riduzione ha associatività da destra a sinistra, per esempio:

// Calcola 2^(3^4). L'elevamento a potenza ha precedenza da destra a sinistra
let a = [2, 3, 4];
a.reduceRight((acc,val) => Math.pow(val,acc)) // => 2.4178516392292583e+24

Si noti che né reduce()reduceRight() accettano un argomento opzionale che specifica il valore this su cui la funzione di riduzione deve essere invocata. L'argomento opzionale del valore iniziale prende il suo posto. Se necessario, si può adoperare il metodo Function.bind() nel caso in cui si ha bisogno che la funzione di riduzione sia invocata come metodo di un particolare oggetto.

Gli esempi mostrati finora sono stati numerici per semplicità, ma reduce() e reduceRight() non sono destinati esclusivamente ai calcoli matematici. Qualsiasi funzione che può combinare due valori (come due oggetti) in un valore dello stesso tipo può essere utilizzata come funzione di riduzione. D'altra parte, gli algoritmi espressi utilizzando riduzioni di array possono rapidamente diventare complessi e difficili da capire, e potrebbe essere più facile leggere, scrivere e ragionare sul codice se si adoperano costrutti di loop regolari per elaborare gli array.