Retour au cours

frontend / javascript

Itérateurs, générateurs et Symbol.iterator

Leçon 71 exercice

Explication

Ce que vous allez apprendre

  • Expliquer le protocole itérateur qui rend for...of possible
  • Implémenter Symbol.iterator sur un objet personnalisé pour le rendre itérable
  • Écrire une fonction génératrice (function*) avec yield
  • Justifier pourquoi un générateur peut représenter une séquence infinie sans saturer la mémoire
  • Distinguer un Symbol d'une clé de chaîne classique et dire pourquoi il ne rentre jamais en collision

Dans quel contexte ?

Une équipe construit une classe Pagination qui charge les résultats d'une API page par page, et voudrait que for (const item of pagination) fonctionne naturellement sur cet objet métier, sans convertir manuellement en tableau à chaque fois (ce qui chargerait toutes les pages en mémoire d'un coup, y compris celles jamais consultées). En implémentant Symbol.iterator avec un générateur qui va chercher chaque page à la demande, l'objet devient itérable nativement, et un simple break dans la boucle arrête proprement les appels réseau superflus.

1. Ce que fait vraiment for...of

Quand vous écrivez for (const x of maCollection), JavaScript a besoin de savoir COMMENT parcourir cette collection élément par élément. Ce n'est pas de la magie : ça repose sur un contrat précis, le protocole itérateur.

2. Le contrat : une méthode next() qui répond

Un objet est "itérable" s'il expose une méthode spéciale, Symbol.iterator, qui retourne un objet avec une méthode next(). Chaque appel de next() renvoie la valeur suivante, jusqu'à signaler explicitement qu'il n'y en a plus.

Élément du protocoleRôle
Symbol.iteratorMéthode qui rend un objet itérable, retourne un itérateur
next()Renvoie { value, done } à chaque appel
done: trueSignale la fin de la séquence
function*Génère automatiquement un objet conforme à ce protocole

Prérequis

Cette leçon suppose une bonne aisance avec les fonctions et les closures (leçon 2) : un générateur est une fonction spéciale qui conserve un état entre deux appels, exactement comme une closure conserve les variables de son environnement.

3. Un raccourci bien plus simple : les générateurs

Écrire ce protocole à la main pour chaque structure serait fastidieux. Une fonction générateur (function*) l'implémente automatiquement grâce au mot-clé yield, qui "met en pause" l'exécution à chaque valeur produite et la reprend exactement là au prochain appel.

4. Produire à la demande, pas tout d'un coup

Ce changement de perspective est important : au lieu de construire un tableau complet en mémoire, on produit les valeurs une par une, à la demande. Cela permet même de représenter des séquences infinies, consommées seulement partiellement.

Astuce

Un générateur infini (while (true) { yield ... }) n'est dangereux que si on le consomme entièrement avec un spread ([...generateur]) ou une boucle sans condition d'arrêt. Combiné à un break dans un for...of, il reste parfaitement sûr et ne calcule que ce qui est réellement demandé.

5. Un mécanisme qui dépasse le simple for...of

Ce protocole ne sert pas qu'à for...of : c'est aussi lui qui rend possible le spread ([...collection]) et la déstructuration sur n'importe quel objet personnalisé, pas seulement les tableaux natifs.

6. Pour finir : les Symbol, des clés jamais en collision

Les Symbol servent à créer des clés de propriété garanties uniques, invisibles aux boucles classiques (Object.keys). C'est exactement ainsi que Symbol.iterator évite d'entrer en collision avec une propriété nommée "iterator" que vous auriez créée par erreur.

Commandes & code

Itérateurs, générateurs et Symbol.iterator

Le protocole qui alimente for...of, le spread et la déstructuration.

js
// Le protocole iterateur : un objet avec une methode next() retournant { value, done }
function creerIterateur(max) {
    let actuel = 0;
    return {
        next() {
            if (actuel < max) {
                return { value: actuel++, done: false };
            }
            return { value: undefined, done: true };
        },
    };
}
const it = creerIterateur(3);
console.log(it.next(), it.next(), it.next(), it.next());

// Symbol.iterator : rendre un objet ITERABLE (utilisable avec for...of, spread, destructuring)
class Plage {
    constructor(debut, fin) {
        this.debut = debut;
        this.fin = fin;
    }
    [Symbol.iterator]() {
        let actuel = this.debut;
        const fin = this.fin;
        return {
            next() {
                return actuel < fin
                    ? { value: actuel++, done: false }
                    : { value: undefined, done: true };
            },
        };
    }
}

for (const n of new Plage(0, 5)) {
    console.log(n);
}
console.log([...new Plage(0, 3)]);          // le spread utilise aussi Symbol.iterator

// --- Generateurs : sucre syntaxique bien plus simple pour ecrire un iterateur ---
function* plageGen(debut, fin) {
    for (let i = debut; i < fin; i++) {
        yield i;             // suspend l'execution, renvoie une valeur
    }
}

for (const n of plageGen(0, 5)) {
    console.log(n);
}

// Un generateur EST un iterateur : consommable manuellement
const gen = plageGen(0, 3);
console.log(gen.next());       // { value: 0, done: false }
console.log(gen.next());        // { value: 1, done: false }

// Generateur infini, consomme a la demande
function* nombresInfinis() {
    let n = 0;
    while (true) yield n++;
}
function prendre(iterable, n) {
    const resultat = [];
    for (const valeur of iterable) {
        if (resultat.length >= n) break;
        resultat.push(valeur);
    }
    return resultat;
}
console.log(prendre(nombresInfinis(), 5));    // [0, 1, 2, 3, 4]

// yield* : delegation vers un sous-generateur
function* interne() {
    yield "a";
    yield "b";
}
function* externe() {
    yield 1;
    yield* interne();          // delegue a "interne"
    yield 2;
}
console.log([...externe()]);    // [1, "a", "b", 2]

// Generateurs pour implementer un iterateur d'arbre (parcours en profondeur)
class Noeud {
    constructor(valeur, enfants = []) {
        this.valeur = valeur;
        this.enfants = enfants;
    }
    *[Symbol.iterator]() {
        yield this.valeur;
        for (const enfant of this.enfants) {
            yield* enfant;              // recursion via delegation
        }
    }
}
const arbre = new Noeud(1, [new Noeud(2, [new Noeud(4)]), new Noeud(3)]);
console.log([...arbre]);    // [1, 2, 4, 3]

// Communication bidirectionnelle avec generator.next(valeur)
function* dialogue() {
    const nom = yield "Quel est ton nom ?";
    const age = yield `Bonjour ${nom}, quel age as-tu ?`;
    return `${nom} a ${age} ans`;
}
const conv = dialogue();
console.log(conv.next());              // demarre, ignore la 1ere valeur envoyee
console.log(conv.next("Alice"));         // envoie "Alice" comme valeur de "yield" precedent
console.log(conv.next(30));               // envoie 30

// Symbol : valeurs uniques, utilisees comme cles de proprietes non-collisionnables
const ID_UNIQUE = Symbol("id");
const objet = { [ID_UNIQUE]: 42, nom: "test" };
console.log(Object.keys(objet));           // ["nom"] -- les Symbols n'apparaissent pas ici
console.log(objet[ID_UNIQUE]);               // 42

Résumé

  • Symbol.iterator rend un objet compatible avec for...of, le spread et la déstructuration.
  • Un générateur (function* + yield) est la façon la plus simple d'implémenter le protocole itérateur.
  • yield* délègue l'itération à un sous-générateur ou sous-itérable, utile pour la récursion.
  • Les Symbol créent des clés de propriété garanties uniques, invisibles pour Object.keys.

Exercices pratiques

1 disponible
1

Mission : rendre une file d'attente compatible for...of

Objectif : Implémenter Symbol.iterator sur une classe personnalisée, et diagnostiquer un générateur infini mal consommé.

Contexte

Technologik gère une classe FileAttente qui stocke des tickets de support, mais for (const ticket of maFile) échoue avec TypeError: maFile is not iterable. Un autre développeur a aussi écrit un générateur infini pour distribuer des numéros de ticket, mais un console.log([...distributeurNumeros()]) a fait planter le serveur de test :

js
class FileAttente {
  constructor() { this.tickets = []; }
  ajouter(t) { this.tickets.push(t); }
}
function* distributeurNumeros() {
  let n = 1;
  while (true) yield n++;
}

Tu dois rendre FileAttente itérable, et corriger l'usage dangereux du générateur infini.

Résoudre l’exercice →