Question 1Juste après avoir appelé une fonction déclarée avec function* et obtenu un générateur, jusqu'où le corps s'est-il exécuté ?
Itérateurs et générateurs — obtenir les valeurs une par une
Les itérateurs fournissent les valeurs une par une, les générateurs s'arrêtent à chaque yield. next(), for...of, spread et suites sans fin.
Quand tu distribues des tickets numérotés un par un, ou que tu affiches une longue liste d'avis quelques-uns à la fois, créer tous les numéros ou toutes les pages dès le départ revient à préparer des éléments qui ne serviront jamais. Tu peux écrire une fonction qui retient la valeur suivante grâce à une closure, mais tu dois alors gérer toi-même où tu en es et si c'est terminé.
Cet article présente les itérateurs, qui renvoient les valeurs une par une, et les générateurs, qui permettent de les écrire sous forme de fonctions.
Des fonctions qui reprennent là où elles s'étaient arrêtées — générateurs et yield
Imagine une fonction qui distribue le numéro suivant à un guichet — 101, puis 102, puis 103 — à chaque appel. Une fonction ordinaire se termine dès qu'elle exécute return, et l'appel suivant repart du début : il faudrait donc une variable extérieure pour retenir où tu en es.
Quand tu appelles une fonction génératrice (une fonction déclarée avec function*, qui peut utiliser yield à l'intérieur), son corps ne s'exécute pas : tu récupères à la place un générateur (un objet qui fait avancer le corps petit à petit). Chaque appel à sa méthode next() exécute le corps jusqu'au yield suivant et s'y arrête. next() renvoie un objet dont value contient la valeur passée à yield et dont done indique si le corps est terminé.
// Déclarer avec function* crée une fonction génératrice
function* callNumbers() {
console.log("Ouverture du guichet");
yield 101;
yield 102;
yield 103;
}
// L'appel n'exécute pas le corps : il renvoie juste un générateur
const numbers = callNumbers();
// Chaque appel à next() avance jusqu'au yield suivant et s'arrête
const first = numbers.next(); // « Ouverture du guichet » s'affiche ici
console.log(`${first.value} / ${first.done}`); // 101 / false
console.log(numbers.next().value); // 102
console.log(numbers.next().value); // 103
// Quand il ne reste plus de yield, done passe à true
const last = numbers.next();
console.log(`${last.value} / ${last.done}`); // undefined / true
Dès que le corps arrive à sa fin ou exécute return, done passe à true, et les appels suivants à next() n'exécutent plus le corps. C'est le générateur lui-même qui retient où il en est, donc tu n'as pas besoin d'une variable extérieure pour mémoriser le numéro du guichet.
Impossible d'écrire yield dans un callback
Tu ne peux écrire yield qu'entre les accolades d'une fonction déclarée avec function*. Une fonction fléchée passée à forEach est une fonction à part : y écrire yield provoque une SyntaxError, et le message d'erreur ne mentionne même pas yield. Pour renvoyer les éléments d'un tableau un par un, écris yield dans le corps d'une boucle for ou for...of.
Boucler sans écrire next() — le protocole itérateur
Imagine que tu veux afficher dans l'ordre le suivi d'une commande : « Reçue », « Expédiée », « En cours de livraison ». Si tu écris un appel à next() par étape, il te faut une ligne par étape, et tu dois en ajouter une chaque fois qu'une nouvelle étape apparaît. Avec une boucle while, tu dois quand même écrire toi-même le test de done et l'appel suivant à next().
La règle selon laquelle chaque appel à next() renvoie un objet avec value et done s'appelle le protocole itérateur, et un objet qui la respecte est un itérateur. Les générateurs de la section précédente sont des itérateurs. for...of appelle next() jusqu'à ce que done vaille true, en plaçant chaque value dans la variable de boucle.
function* deliverySteps() {
yield "Reçue";
yield "Expédiée";
yield "En cours de livraison";
}
// for...of appelle next() jusqu'à ce que done vaille true
for (const step of deliverySteps()) {
console.log(step); // 3 lignes : Reçue / Expédiée / En cours de livraison
}
// Le même comportement écrit avec while et next()
const steps = deliverySteps();
let result = steps.next();
while (!result.done) {
console.log(result.value); // 3 lignes : Reçue / Expédiée / En cours de livraison
result = steps.next();
}
Tu peux passer à for...of n'importe quelle valeur itérable (une valeur dont for...of peut obtenir un itérateur), comme un tableau, une Map ou un générateur. for...of ne regarde pas de quel type de valeur il s'agit : il se contente d'appeler next() sur l'itérateur obtenu, donc tu les parcours tous de la même façon. Le tableau ci-dessous montre ce que reçoit la variable de boucle.
| Ce que tu passes à for...of | Variable de boucle à chaque tour | Remarques |
|---|---|---|
| Le tableau ["Reçue", "Expédiée"] | Chaque élément ("Reçue" → "Expédiée") | Chaque boucle crée un nouvel itérateur qui repart du premier élément |
| Map | Un tableau [clé, valeur] de 2 éléments | Les entrées sortent dans leur ordre d'ajout |
| Générateur | Les valeurs écrites après yield | Il est son propre itérateur, donc la progression faite avec next() est conservée |
| Un objet comme { name: "Stylo" } | Rien (TypeError: … is not iterable) | Il n'est pas itérable : pour parcourir ses clés, utilise for...in ou Object.entries |
Tout transformer en tableau — syntaxe de décomposition et générateur à usage unique
Imagine que tu veux afficher les créneaux de retrait sur une seule ligne, comme « 10:00 / 13:00 / 16:00 ». Un générateur n'est pas un tableau : il n'a ni join ni length, et tel quel, tu ne peux même pas compter ses valeurs. Ajouter chaque valeur à un tableau avec push dans un for...of rajoute des lignes rien que pour ça.
Si tu places un générateur entre les crochets d'un tableau avec la syntaxe de décomposition ... (spread), il extrait les valeurs jusqu'à ce que done vaille true et les range dans l'ordre dans un nouveau tableau. Une fois que tu as écrit const slotList = [...slots];, tu peux utiliser aussi bien join que length.
function* pickupSlots() {
yield "10:00";
yield "13:00";
yield "16:00";
}
// Un générateur n'est pas un tableau, il n'a donc pas de length
const slots = pickupSlots();
console.log(slots.length); // undefined
// La décomposition dans [] range les valeurs dans l'ordre
const slotList = [...slots];
console.log(slotList.join(" / ")); // 10:00 / 13:00 / 16:00
// Décomposer à nouveau un générateur épuisé donne un tableau vide
console.log([...slots].length); // 0
// Rappeler la fonction donne un nouveau générateur qui repart du début
console.log([...pickupSlots()].length); // 3
Quand tu as besoin des mêmes valeurs à plusieurs endroits, décompose-les une seule fois dans un tableau, stocke-le dans une variable et réutilise ce tableau. Un générateur que tu as déjà parcouru jusqu'au bout avec for...of donne lui aussi un tableau vide si tu le décomposes ensuite.
Ne créer que ce qu'il faut — évaluation paresseuse et suites sans fin
Quand tu numérotes des annonces à partir de 1, tu ne sais pas à l'avance combien il y en aura. Si tu crées d'emblée 1000 numéros dans un tableau, tous sont calculés même si tu n'en utilises que 3. Et sans limite fixée, le tableau ne serait même jamais terminé.
Les générateurs permettent l'évaluation paresseuse (calculer une valeur seulement au moment où on en a besoin). Même une fonction qui fait yield sans fin dans un while (true) n'exécute son corps qu'autant de fois que next() a été appelé. La déstructuration de tableau fonctionne aussi sur les générateurs : elle extrait exactement autant de valeurs que tu en listes à gauche.
function* serialFrom(start) {
let number = start;
while (true) {
console.log(`${number} créé`);
yield number;
number++;
}
}
// La déstructuration extrait autant de valeurs que tu en listes à gauche
const [first, second] = serialFrom(1); // 1 créé / 2 créé
console.log(`${first}, ${second}`); // 1, 2
// for...of arrête d'extraire des valeurs dès que tu sors avec break
for (const number of serialFrom(1)) {
console.log(`Annonce n° ${number}`);
if (number === 3) break;
}
// 1 créé / Annonce n° 1 / 2 créé / Annonce n° 2
// 3 créé / Annonce n° 3 (4 n'est jamais créé)
Pour transformer une suite sans fin en tableau, intercale un autre générateur qui prend un nombre fixé de valeurs puis fait return. Quand ce générateur intermédiaire se termine, done passe à true, et la décomposition s'arrête elle aussi à ce moment-là.
Si aucun yield n'est atteint, next() ne rend jamais la main
Si tu écris une boucle while (true) qui ne fait yield que lorsqu'une condition if est remplie, et que cette condition ne l'est jamais, next() ne rend jamais la main. L'exécution ne se termine pas, et les lignes suivantes ne s'exécutent jamais. Quand tu boucles sans fin, vérifie d'abord qu'une valeur remplissant la condition finira forcément par arriver.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Si tu exécutes deux fois de suite [...slots] sur un générateur slots qui fait yield de 3 valeurs, quelle est la longueur la deuxième fois ?
Question 3Pour un générateur gen qui fait yield sans fin dans un while (true), laquelle de ces écritures ne se termine jamais ?