Iterabili e Iteratori JavaScript
Scopri gli iterabili JavaScript: protocollo degli iteratori, generatori, sequenze infinite e tecniche pratiche per lavorare con le collezioni.
Introduzione agli Iterabili JavaScript
Gli iterabili JavaScript sono object che implementano un protocollo specifico, permettendo loro di essere consumati da costrutti di iterazione come for...of. Questa guida tratta i concetti fondamentali, gli iterabili integrati e le tecniche pratiche per lavorare con le collezioni.
Cosa sono gli Iterabili in JavaScript?
Alla base, un iterabile è un object che implementa il metodo Symbol.iterator, consentendo l'accesso sequenziale ai suoi elementi. Diversi tipi integrati in JavaScript sono iterabili, tra cui Array, String, Map, Set e altri. È importante distinguere tra un iterabile (l'object che viene percorso) e un iteratore (l'object restituito da Symbol.iterator che esegue effettivamente il percorso). Questi iterabili sono fondamentali per varie operazioni, come il ciclo e la manipolazione dei dati.
Per un'esplorazione dettagliata degli object Map e Set di JavaScript, consulta la nostra guida completa su JavaScript Map e Set.
Esempio di Iterabile: Array
Vediamo un esempio di base di un iterabile in JavaScript:
Questo frammento di codice dimostra l'iterazione su un array di frutti, un iterabile comune. Il ciclo for...of chiama automaticamente il metodo Symbol.iterator dell'iterabile e consuma l'iteratore risultante finché done non è true. Consulta i cicli JavaScript per la famiglia completa dei costrutti di ciclo.
Non confondere for...of con for...in. for...of itera sui valori prodotti da un iterabile (elementi di array, caratteri di string, voci di Map). for...in itera sulle chiavi delle proprietà enumerabili di un object — incluse quelle ereditate — ed è pensato per object semplici, non per array. Usare for...in su un array restituisce le stringhe degli indici ("0", "1", …) e può intercettare proprietà aggiuntive, quindi preferire for...of per i dati ordinati.
Il Protocollo dell'Iteratore
Il cardine di un iterabile è il suo metodo Symbol.iterator. Il protocollo è un contratto preciso:
- Un iterabile ha un metodo identificato da
Symbol.iterator. Chiamarlo restituisce un iteratore. - Un iteratore è un object con un metodo
next(). - Ogni chiamata a
next()restituisce un object{ value, done }:done: false—valueè il prossimo elemento nella sequenza.done: true— l'iterazione è terminata (valueviene quindi ignorato, o contiene un risultato finale opzionale).
Qualsiasi object che segue questo contratto funziona con for...of, l'operatore spread, la destrutturazione e Array.from() — anche se l'object è stato scritto da te. Symbol è una chiave unica integrata; consulta il capitolo Tipo Symbol per capire perché questi metodi di protocollo ne usano uno invece di un nome string semplice.
Esempio: un iterabile range personalizzato
Un caso d'uso classico è un object che produce un intervallo numerico in modo pigro, senza mai costruire un array:
In questo esempio, il metodo [Symbol.iterator]() restituisce un iteratore fresco ogni volta, quindi lo stesso object range può essere percorso più di una volta. La sintassi abbreviata del metodo assicura che this faccia correttamente riferimento all'object range. (Usare una funzione arrow per [Symbol.iterator] catturerebbe this lessicalmente e romperebbe il pattern.)
Generatori: il modo semplice per costruire iterabili
Scrivere next() e tenere traccia dello stato manualmente è verboso. Una funzione generatore — dichiarata con function* e che usa yield — produce un iteratore automaticamente. Ogni yield mette in pausa la funzione e passa un valore al consumatore; l'esecuzione riprende alla successiva chiamata a next(). Lo stesso range diventa molto più breve:
L'* prima del nome del metodo lo rende un metodo generatore, quindi range è iterabile con pochissimo boilerplate. Per tutto ciò che i generatori possono fare — inclusa la comunicazione bidirezionale e la delega con yield* — consulta Generatori e Iteratori e generatori asincroni.
Sequenze infinite e pigre
Poiché un iteratore calcola il valore successivo solo quando richiesto, può descrivere sequenze troppo grandi — o addirittura infinite — per essere tenute in memoria. Questo è il motivo principale per cui si scrive un iteratore personalizzato invece di usare semplicemente un array:
Non usare mai lo spread ([...naturals()]) o for...of senza un break su un iterabile infinito — andrà in loop per sempre. Preleva un numero finito di valori con next().
Consumare gli Iterabili
Una volta che un object è iterabile, l'intero linguaggio si apre ad esso: qualsiasi costrutto che accetta un iterabile funziona con il tuo tipo personalizzato allo stesso modo in cui funziona con gli array.
Usare Array.from()
Il metodo Array.from() crea un nuovo array da qualsiasi object iterabile (o array-like). Accetta anche una funzione map opzionale come secondo argomento, applicata a ogni elemento mentre l'array viene costruito — più pratico di Array.from(it).map(fn) perché evita un secondo passaggio:
Consulta JavaScript Map e Set per ulteriori informazioni su Set, e Metodi degli array per ciò che puoi fare una volta ottenuto un array.
Sintassi Spread con gli Iterabili
La sintassi spread (...) espande un iterabile ovunque siano attesi argomenti o elementi — per unire array, copiare, o passare elementi come argomenti di funzione:
Per il quadro completo di ... in posizione sia spread che collect, consulta Parametri rest e sintassi spread.
Destrutturazione e rest
L'assegnazione per destrutturazione estrae i valori da qualsiasi iterabile per posizione, e il pattern rest (...) raccoglie il resto in un array:
Scopri di più in Assegnazione per destrutturazione.
L'iterabile String e la sicurezza Unicode
Le string sono iterabili, e soprattutto l'iteratore percorre i code point Unicode, non le unità di codice a 16 bit. Ciò significa che i caratteri a coppia surrogata (emoji, alcuni script) rimangono intatti — a differenza dell'indicizzazione con [i] o i vecchi cicli for su .length, che possono dividerli:
Ogni volta che hai bisogno di contare o suddividere correttamente i caratteri visibili all'utente, itera la string (o espandila con spread) invece di fare affidamento su .length. Consulta il capitolo String per ulteriori informazioni.
Riepilogo
- Un object è iterabile quando ha un metodo
[Symbol.iterator]()che restituisce un iteratore — un object il cuinext()produce{ value, done }. - Preferisci un generatore (
function*/yield) invece di scriverenext()a mano; è il modo più breve e meno soggetto a errori per rendere qualcosa iterabile. - Usa
for...ofper i valori di dati ordinati/iterabili; usafor...insolo per le chiavi di object semplici. - Una volta iterabile, il tuo tipo si integra con spread, destrutturazione, rest,
Array.from(it, mapFn)e molte API integrate gratuitamente. - Gli iteratori sono pigri, quindi possono modellare sequenze infinite o enormi che un array non potrebbe mai contenere.
- Itera le string (non indicizzarle) per gestire i caratteri Unicode come le emoji in modo sicuro.