ForEach

Edoardo Midali
Edoardo Midali

Il metodo forEach rappresenta un approccio funzionale per iterare sugli elementi di un array, eseguendo una funzione callback per ciascun elemento. A differenza dei cicli tradizionali, forEach enfatizza l’intento dichiarativo dell’iterazione, separando la logica di attraversamento dalla logica di elaborazione.

Fondamenti e Sintassi

Il metodo forEach è disponibile su tutti gli array e accetta una funzione callback che riceve fino a tre parametri: l’elemento corrente, l’indice, e l’array completo. La funzione viene invocata per ogni elemento nell’ordine dell’array.

const numeri = [1, 2, 3, 4, 5];

numeri.forEach((numero) => {
  console.log(numero * 2);
});

// Con tutti i parametri
numeri.forEach((elemento, indice, array) => {
  console.log(`Elemento ${elemento} all'indice ${indice}`);
  console.log(`Array ha lunghezza ${array.length}`);
});

Tipizzazione della Callback

TypeScript inferisce automaticamente i tipi dei parametri della callback basandosi sul tipo dell’array. È possibile specificare tipi espliciti quando necessario per maggiore chiarezza o per callback riutilizzabili.

interface Prodotto {
  nome: string;
  prezzo: number;
}

const prodotti: Prodotto[] = [
  { nome: "Libro", prezzo: 15 },
  { nome: "Penna", prezzo: 2 },
];

prodotti.forEach((prodotto: Prodotto) => {
  console.log(`${prodotto.nome}: €${prodotto.prezzo}`);
});

// Callback tipizzata separatamente
const stampaProdotto = (p: Prodotto, i: number): void => {
  console.log(`${i + 1}. ${p.nome}`);
};

prodotti.forEach(stampaProdotto);

Valore di Ritorno

Una caratteristica distintiva di forEach è che restituisce sempre undefined. Non è possibile usare forEach per trasformare o ridurre un array, a differenza di map, filter o reduce. forEach è pensato esclusivamente per effetti collaterali.

const risultato = [1, 2, 3].forEach((n) => n * 2);
console.log(risultato); // undefined

// Per trasformazioni, usare map
const trasformato = [1, 2, 3].map((n) => n * 2);
console.log(trasformato); // [2, 4, 6]

Controllo di Flusso

forEach non supporta break o continue. Una volta avviato, itera attraverso tutti gli elementi. Per interrompere anticipatamente, è necessario usare altri costrutti come for…of o find/some.

const numeri = [1, 2, 3, 4, 5];

// Non è possibile interrompere forEach
numeri.forEach((n) => {
  if (n === 3) {
    // break; // ERRORE: Illegal break statement
    return; // Salta solo questa iterazione, continua con le successive
  }
  console.log(n);
}); // Output: 1, 2, 4, 5

// Per uscita anticipata, usare for...of
for (const n of numeri) {
  if (n === 3) break;
  console.log(n);
} // Output: 1, 2

Contesto this

forEach accetta un secondo parametro opzionale che specifica il valore di this all’interno della callback. Questo è utile quando la callback deve accedere a proprietà di un oggetto contenitore.

class Processore {
  moltiplicatore: number = 10;

  processa(numeri: number[]): void {
    numeri.forEach(function (n) {
      console.log(n * this.moltiplicatore);
    }, this); // Passa il contesto this
  }

  // Con arrow function, this è catturato lessicalmente
  processaConArrow(numeri: number[]): void {
    numeri.forEach((n) => {
      console.log(n * this.moltiplicatore);
    });
  }
}

ForEach con Operazioni Asincrone

forEach non attende Promise, rendendo problematico l’uso con operazioni asincrone. Per elaborazioni asincrone sequenziali o parallele, preferire for…of con await o Promise.all.

const ids = [1, 2, 3];

// PROBLEMATICO: forEach non attende
ids.forEach(async (id) => {
  const dati = await fetchDati(id);
  console.log(dati);
}); // Le chiamate partono tutte insieme

// CORRETTO: for...of per esecuzione sequenziale
for (const id of ids) {
  const dati = await fetchDati(id);
  console.log(dati);
}

// CORRETTO: Promise.all per esecuzione parallela
await Promise.all(
  ids.map(async (id) => {
    const dati = await fetchDati(id);
    console.log(dati);
  })
);

Modifica dell’Array Durante Iterazione

Modificare l’array durante l’iterazione può causare comportamenti imprevisti. forEach itera sugli indici originali, quindi elementi aggiunti durante l’iterazione potrebbero non essere visitati, mentre elementi rimossi potrebbero causare salti.

const numeri = [1, 2, 3, 4];

// Comportamento potenzialmente confuso
numeri.forEach((n, i) => {
  if (n % 2 === 0) {
    numeri.splice(i, 1); // Modifica durante iterazione
  }
});
// Risultato imprevedibile

// Approccio corretto: creare nuovo array
const dispari = numeri.filter((n) => n % 2 !== 0);

ForEach vs Altri Metodi di Iterazione

La scelta tra forEach e alternative dipende dal caso d’uso. forEach è appropriato per effetti collaterali come logging, aggiornamento UI, o operazioni I/O, ma non per trasformazioni dati.

const prodotti = [
  { nome: "A", prezzo: 10 },
  { nome: "B", prezzo: 20 },
];

// forEach per effetti collaterali
prodotti.forEach((p) => {
  salvaSuDatabase(p);
  inviaNotifica(p);
});

// map per trasformazioni
const nomi = prodotti.map((p) => p.nome);

// filter per selezione
const costosi = prodotti.filter((p) => p.prezzo > 15);

// reduce per aggregazione
const totale = prodotti.reduce((sum, p) => sum + p.prezzo, 0);

Performance

forEach ha overhead leggermente superiore rispetto a for classico a causa della chiamata di funzione per ogni elemento. Per array molto grandi o iterazioni critiche per performance, un ciclo for tradizionale può essere più veloce.

const grande = new Array(1000000).fill(0);

// Leggermente più lento
console.time("forEach");
grande.forEach((n) => n * 2);
console.timeEnd("forEach");

// Leggermente più veloce
console.time("for");
for (let i = 0; i < grande.length; i++) {
  grande[i] * 2;
}
console.timeEnd("for");

Casi d’Uso Comuni

forEach eccelle quando si devono eseguire operazioni laterali che non producono un nuovo valore di ritorno: aggiornamento DOM, logging, invio richieste, accumulo in strutture esterne.

// Aggiornamento UI
utenti.forEach((utente) => {
  const elemento = document.createElement("li");
  elemento.textContent = utente.nome;
  lista.appendChild(elemento);
});

// Accumulo in Map
const raggruppati = new Map<string, Prodotto[]>();
prodotti.forEach((p) => {
  const categoria = p.categoria;
  if (!raggruppati.has(categoria)) {
    raggruppati.set(categoria, []);
  }
  raggruppati.get(categoria)!.push(p);
});

// Logging strutturato
operazioni.forEach((op, i) => {
  logger.info(`Operazione ${i}: ${op.tipo}`, {
    timestamp: Date.now(),
    dettagli: op,
  });
});

ForEach su Altre Strutture

Oltre agli array, forEach è disponibile su Map, Set e NodeList, con comportamenti leggermente diversi che riflettono la natura di ciascuna struttura.

// Map: callback riceve (valore, chiave, map)
const mappa = new Map([
  ["a", 1],
  ["b", 2],
]);
mappa.forEach((valore, chiave) => {
  console.log(`${chiave}: ${valore}`);
});

// Set: callback riceve (valore, valore, set)
const insieme = new Set([1, 2, 3]);
insieme.forEach((valore) => {
  console.log(valore * 2);
});

// NodeList
document.querySelectorAll(".elemento").forEach((nodo) => {
  nodo.classList.add("attivo");
});

Considerazioni di Design

Usare forEach quando l’intento è eseguire azioni per ogni elemento senza produrre un valore di ritorno. Preferire map/filter/reduce quando si trasformano dati. Evitare forEach con operazioni asincrone o quando serve controllo di flusso come break/continue. La chiarezza di intento dovrebbe guidare la scelta: forEach comunica “esegui questa azione per ogni elemento”, mentre altri costrutti comunicano intenti diversi.

Il metodo forEach fornisce quindi un’astrazione pulita per iterazioni con effetti collaterali, separando l’attraversamento dalla logica, migliorando leggibilità quando usato appropriatamente nel contesto giusto.