Iteratori JavaScript

Gli iteratori in JavaScript sono oggetti che implementano un protocollo specifico per attraversare sequenze di dati. Rappresentano il meccanismo fondamentale che permette ai costrutti come for...of, destructuring e operatori spread di funzionare con diversi tipi di collezioni. Comprendere gli iteratori significa capire come JavaScript gestisce l’iterazione a livello profondo.
Il Protocollo Iteratore
Un iteratore è semplicemente un oggetto che implementa il metodo next(). Questo metodo restituisce un oggetto con due proprietà: value (il valore corrente) e done (booleano che indica se l’iterazione è completata). Questa semplicità concettuale nasconde una potenza notevole nel gestire qualsiasi tipo di sequenza dati.
// Esempio base di un iteratore manuale
function creaIteratoreNumeri(max) {
let corrente = 0;
return {
next: function () {
if (corrente < max) {
return { value: corrente++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
}
const iteratore = creaIteratoreNumeri(3);
console.log(iteratore.next()); // { value: 0, done: false }
console.log(iteratore.next()); // { value: 1, done: false }
console.log(iteratore.next()); // { value: 2, done: false }
console.log(iteratore.next()); // { value: undefined, done: true }
Quando done diventa true, l’iterazione è considerata completata e le chiamate successive a next() dovrebbero continuare a restituire { value: undefined, done: true }.
Oggetti Iterabili
Un oggetto è iterabile se implementa il metodo Symbol.iterator, che deve restituire un iteratore. Tutti gli oggetti iterabili possono essere utilizzati con for...of, destructuring e operatori spread. Gli array, le stringhe, le Map, le Set e molti altri oggetti built-in sono già iterabili.
// Esempio di oggetto iterabile personalizzato
class RangeNumeri {
constructor(inizio, fine) {
this.inizio = inizio;
this.fine = fine;
}
// Implementa Symbol.iterator per rendere l'oggetto iterabile
[Symbol.iterator]() {
let corrente = this.inizio;
const fine = this.fine;
return {
next() {
if (corrente <= fine) {
return { value: corrente++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
}
}
const range = new RangeNumeri(1, 5);
// Ora possiamo usare for...of
for (const numero of range) {
console.log(numero); // 1, 2, 3, 4, 5
}
// E anche destructuring
const [primo, secondo, ...resto] = range;
console.log(primo, secondo, resto); // 1, 2, [3, 4, 5]
// E spread operator
const array = [...range];
console.log(array); // [1, 2, 3, 4, 5]
Iteratori degli Oggetti Built-in
Array e i Suoi Iteratori
Gli array forniscono diversi iteratori per different modalità di accesso:
const array = ["a", "b", "c"];
// Iteratore dei valori (predefinito)
for (const valore of array) {
console.log(valore); // 'a', 'b', 'c'
}
// Iteratore degli indici
for (const indice of array.keys()) {
console.log(indice); // 0, 1, 2
}
// Iteratore delle coppie [indice, valore]
for (const [indice, valore] of array.entries()) {
console.log(indice, valore); // 0 'a', 1 'b', 2 'c'
}
// Accesso diretto agli iteratori
const iteratoreValori = array.values();
const iteratoreChiavi = array.keys();
const iteratoreEntries = array.entries();
console.log(iteratoreValori.next()); // { value: 'a', done: false }
console.log(iteratoreChiavi.next()); // { value: 0, done: false }
console.log(iteratoreEntries.next()); // { value: [0, 'a'], done: false }
Map e Set
Le collezioni Map e Set hanno iteratori nativi che permettono di attraversare i loro elementi:
const mappa = new Map([
["nome", "Mario"],
["età", 30],
["città", "Roma"],
]);
// Iterazione delle chiavi
for (const chiave of mappa.keys()) {
console.log(chiave); // 'nome', 'età', 'città'
}
// Iterazione dei valori
for (const valore of mappa.values()) {
console.log(valore); // 'Mario', 30, 'Roma'
}
// Iterazione delle coppie (predefinito per Map)
for (const [chiave, valore] of mappa) {
console.log(`${chiave}: ${valore}`);
}
const insieme = new Set(["rosso", "verde", "blu"]);
// Set itera sui valori
for (const colore of insieme) {
console.log(colore); // 'rosso', 'verde', 'blu'
}
Stringhe come Iterabili
Le stringhe sono iterabili carattere per carattere, gestendo correttamente i caratteri Unicode:
const emoji = "👋🌍✨";
for (const carattere of emoji) {
console.log(carattere); // 👋, 🌍, ✨
}
// Conversione in array rispettando Unicode
const caratteri = [...emoji];
console.log(caratteri); // ['👋', '🌍', '✨']
console.log(caratteri.length); // 3 (non 6 come con .length)
Creazione di Iteratori Avanzati
Iteratore per Strutture Dati Personalizzate
class AlberoPersonalizzato {
constructor() {
this.radice = null;
}
aggiungi(valore) {
if (!this.radice) {
this.radice = { valore, figli: [] };
} else {
this.radice.figli.push({ valore, figli: [] });
}
}
// Iteratore depth-first
[Symbol.iterator]() {
const stack = this.radice ? [this.radice] : [];
return {
next() {
if (stack.length === 0) {
return { done: true };
}
const nodo = stack.pop();
// Aggiungi i figli allo stack (in ordine inverso per mantenere l'ordine)
for (let i = nodo.figli.length - 1; i >= 0; i--) {
stack.push(nodo.figli[i]);
}
return { value: nodo.valore, done: false };
},
};
}
}
const albero = new AlberoPersonalizzato();
albero.aggiungi("radice");
albero.radice.figli.push({ valore: "figlio1", figli: [] });
albero.radice.figli.push({ valore: "figlio2", figli: [] });
for (const valore of albero) {
console.log(valore); // radice, figlio1, figlio2
}
Iteratore Infinito con Controllo
class SequenzaFibonacci {
constructor(limite = null) {
this.limite = limite;
}
[Symbol.iterator]() {
let a = 0,
b = 1,
contatore = 0;
const limite = this.limite;
return {
next() {
if (limite !== null && contatore >= limite) {
return { done: true };
}
const valore = a;
[a, b] = [b, a + b];
contatore++;
return { value: valore, done: false };
},
};
}
}
// Fibonacci limitato
const fibLimitato = new SequenzaFibonacci(10);
console.log([...fibLimitato]); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Fibonacci infinito (attenzione nei loop!)
const fibInfinito = new SequenzaFibonacci();
const iteratoreFib = fibInfinito[Symbol.iterator]();
// Prendi solo i primi 5
for (let i = 0; i < 5; i++) {
console.log(iteratoreFib.next().value); // 0, 1, 1, 2, 3
}
Iteratori e Metodi di Array
Molti metodi di array funzionano con qualsiasi oggetto iterabile:
// Array.from converte iterabili in array
const range = new RangeNumeri(1, 5);
const arrayDaRange = Array.from(range);
console.log(arrayDaRange); // [1, 2, 3, 4, 5]
// Con funzione di trasformazione
const arrayRaddoppiato = Array.from(range, (x) => x * 2);
console.log(arrayRaddoppiato); // [2, 4, 6, 8, 10]
// Creazione array da stringa (rispetta Unicode)
const caratteriEmoji = Array.from("👋🌍");
console.log(caratteriEmoji); // ['👋', '🌍']
Composizione e Chaining di Iteratori
class IteratoreTransformatore {
constructor(iterabile, trasformatore) {
this.iterabile = iterabile;
this.trasformatore = trasformatore;
}
[Symbol.iterator]() {
const iteratore = this.iterabile[Symbol.iterator]();
const trasformatore = this.trasformatore;
return {
next() {
const risultato = iteratore.next();
if (risultato.done) {
return risultato;
}
return {
value: trasformatore(risultato.value),
done: false,
};
},
};
}
}
class IteratoreFiltro {
constructor(iterabile, predicato) {
this.iterabile = iterabile;
this.predicato = predicato;
}
[Symbol.iterator]() {
const iteratore = this.iterabile[Symbol.iterator]();
const predicato = this.predicato;
return {
next() {
while (true) {
const risultato = iteratore.next();
if (risultato.done || predicato(risultato.value)) {
return risultato;
}
}
},
};
}
}
// Composizione di iteratori
const numeri = new RangeNumeri(1, 10);
const pari = new IteratoreFiltro(numeri, (x) => x % 2 === 0);
const pariRaddoppiati = new IteratoreTransformatore(pari, (x) => x * 2);
console.log([...pariRaddoppiati]); // [4, 8, 12, 16, 20]
Vantaggi e Casi d’Uso
Lazy Evaluation: Gli iteratori calcolano valori solo quando richiesti, permettendo di lavorare con sequenze infinite o molto grandi senza problemi di memoria.
Interfaccia Uniforme: Tutti gli oggetti iterabili possono essere utilizzati con gli stessi costrutti linguistici (for...of, destructuring, spread).
Composizione: Gli iteratori possono essere facilmente composti per creare pipeline di trasformazione dati efficiente.
Controllo Preciso: Permettono controllo fine grained sull’iterazione, includendo pausa, ripresa e terminazione anticipata.
Gli iteratori rappresentano un esempio perfetto di come JavaScript combini semplicità concettuale con potenza espressiva. Forniscono un’astrazione elegante per traversare qualsiasi tipo di collezione o sequenza, mantenendo il codice pulito e le performance ottimali attraverso la lazy evaluation. Comprendere gli iteratori è essenziale per scrivere JavaScript moderno ed efficiente.
