Iteratori JavaScript

Edoardo Midali
Edoardo Midali

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.