Le protocole d'itération
Beaucoup de fonctionnalités JavaScript — for...of, le spread (...), la déstructuration, Array.from, Promise.all — reposent toutes sur un même mécanisme sous-jacent : le protocole d'itération. Une fois qu'on a saisi le principe, elles apparaissent toutes comme des variantes d'une même idée.
Un itérateur, c'est tout objet qui possède une méthode next() renvoyant { value, done } :
Appelez next() en boucle. Chaque appel renvoie la valeur suivante accompagnée d'un drapeau done. Quand done passe à true, la séquence est terminée. Voilà tout le protocole — quatre caractères tapés et un booléen.
Itérable vs itérateur
Il existe un second concept, étroitement lié. Un itérable, c'est tout objet capable de produire un itérateur. Il le fait via une méthode rangée sous une clé spéciale : Symbol.iterator.
Les tableaux sont des itérables. Appeler numbers[Symbol.iterator]() renvoie un nouvel itérateur. Les chaînes, Map, Set et arguments sont également des itérables — et c'est précisément pour ça que for...of fonctionne avec tous ces objets.
La distinction est importante : l'itérable, c'est la collection ; l'itérateur, c'est le curseur. Une même itérable peut te fournir autant de curseurs indépendants que tu le souhaites.
Pourquoi for...of fonctionne
for...of n'est rien d'autre que du sucre syntaxique par-dessus le protocole d'itération. En coulisses, il appelle Symbol.iterator, puis next() jusqu'à ce que done vaille true :
Le spread et la déstructuration font exactement la même chose : ils parcourent un itérateur jusqu'à son épuisement :
N'importe quel objet que vous concevez et qui implémente Symbol.iterator profite gratuitement de toutes ces fonctionnalités.
Créer un itérable personnalisé
Construisons un objet range qui produit les nombres de start jusqu'à end :
Quelques points à remarquer :
[Symbol.iterator]()utilise un nom de méthode calculé. La clé, c'est le symbole lui-même, pas la chaîne"Symbol.iterator".- Chaque appel à
[Symbol.iterator]()renvoie un tout nouvel itérateur avec son proprecurrent. C'est précisément ce qui permet de parcourirrangedeux fois sans qu'il soit « épuisé ». - L'itérateur renvoyé n'a besoin que de
next(). Rien de plus.
Ça marche, mais c'est verbeux. Il existe une bien meilleure approche.
Place aux générateurs
Une fonction génératrice se déclare avec function* (notez l'étoile). Au lieu de s'exécuter d'un trait jusqu'à la fin, elle peut se mettre en pause sur une expression yield et reprendre plus tard. L'appeler n'exécute pas son corps : ça renvoie un objet générateur qui est à la fois un itérateur et un itérable :
Chaque appel à next() exécute le corps de la fonction jusqu'au prochain yield, met l'exécution en pause, puis renvoie { value, done: false }. Quand la fonction arrive à son terme, on récupère { value: undefined, done: true }.
Et comme les générateurs sont eux-mêmes des itérables, ils fonctionnent avec tout ce qu'on a vu dans la section précédente :
Réécrire range avec une fonction génératrice
Comparez la version verbeuse du dessus avec celle-ci :
Et voilà. L'étoile * devant [Symbol.iterator] en fait une méthode génératrice. Le yield i remplace à lui seul tout l'objet itérateur qu'on écrivait à la main. Plus de next, plus de done, plus de risque de décalage d'indice — juste une boucle classique avec yield au lieu de push.
C'est exactement pour ça que les générateurs existent. On passe de « écrire un itérateur » à « écrire une fonction qui produit des valeurs avec yield ».
yield vs return
yield met en pause ; return met fin. Tu peux faire yield autant de fois que tu veux — le générateur reprend là où il s'était arrêté :
Un return à l'intérieur d'une fonction génératrice apparaît sous la forme { value: "done", done: true } lors de l'appel qui la termine. for...of et le spread ignorent cette valeur retournée : ils ne consomment que les éléments où done vaut false. Donc n'essayez pas d'utiliser return value pour glisser un dernier élément dans une boucle, il sera tout simplement ignoré.
Séquences paresseuses et infinies
Les générateurs produisent des valeurs à la demande, une par une. Autrement dit, on peut représenter des séquences qui seraient impossibles à stocker dans un tableau :
La boucle est littéralement un while (true), et pourtant le programme se termine — tout simplement parce que le générateur n'avance que lorsqu'on lui demande la valeur suivante. Tu peux prendre les N premiers éléments, t'arrêter là, et le reste ne s'exécutera jamais :
take est lui-même un générateur qui en enveloppe un autre. Composer des générateurs de cette manière fait tout leur charme : des petits blocs, chacun avec une responsabilité unique.
Déléguer avec yield*
Quand une fonction génératrice doit produire toutes les valeurs d'un autre itérable, yield* se charge de la délégation :
yield* fonctionne avec n'importe quel itérable — tableaux, Set, autres générateurs — et transmet chaque élément un par un. C'est l'équivalent du spread, mais côté itérateur.
Les générateurs asynchrones, en deux mots
Un générateur déclaré avec async function* peut faire un yield sur des valeurs qui mettent du temps à arriver — pratique pour streamer depuis une API ou lire un fichier par morceaux. Pour le consommer, on utilise for await...of :
async function* paginate(url) {
let next = url;
while (next) {
const res = await fetch(next);
const page = await res.json();
for (const item of page.items) yield item;
next = page.nextUrl;
}
}
for await (const item of paginate("/api/users")) {
console.log(item);
}
Ce bout de code n'est pas exécutable tel quel (il faut un vrai endpoint), mais c'est bon à savoir que cette forme existe. Une fois que vous maîtrisez les générateurs classiques, les générateurs asynchrones reposent sur la même idée, avec quelques await en plus.
Quand utiliser un générateur ?
Sortez-le de votre boîte à outils quand :
- La séquence est infinie, ou pourrait l'être — identifiants, timestamps, délais entre deux tentatives.
- Produire toutes les valeurs coûte cher et le consommateur peut s'arrêter en cours de route.
- Vous implémentez
Symbol.iteratorsur un objet personnalisé. C'est presque toujours plus court que d'écrire l'objet{ next() }à la main. - Vous voulez composer des transformations en flux (
take,filter,map) sans construire de tableaux intermédiaires.
Restez sur un simple tableau quand les données sont déjà en mémoire et de taille raisonnable. Les générateurs ont un coût : la mécanique qui suspend et reprend une fonction consomme des ressources, et les traces de pile au travers du code d'un générateur sont parfois plus pénibles à lire.
La suite : les symboles
Symbol.iterator est le premier symbole que la plupart des gens rencontrent, mais il est loin d'être le seul. Les symboles sont un type primitif pensé précisément pour ce genre de point d'extension — des clés uniques qui permettent au langage (et à votre propre code) de s'accrocher aux objets sans entrer en collision avec les noms de propriétés ordinaires. C'est le sujet de la page suivante.
Questions fréquentes
Quelle est la différence entre un itérable et un itérateur en JavaScript ?
Un itérable, c'est tout objet qui possède une méthode Symbol.iterator renvoyant un itérateur. L'itérateur, lui, est l'objet qui produit réellement les valeurs : il expose une méthode next() qui retourne { value, done }. Les Array, les chaînes, les Map et les Set sont itérables ; appeler leur méthode Symbol.iterator te donne un itérateur que tu peux parcourir étape par étape.
C'est quoi une fonction génératrice en JavaScript ?
C'est une fonction déclarée avec function* qui produit des valeurs à la demande via yield. L'appeler n'exécute pas le corps : ça renvoie un objet générateur qui est à la fois itérateur et itérable. Chaque appel à next() exécute le code jusqu'au prochain yield, met en pause, et renvoie la valeur cédée.
Quelle différence entre yield et return dans un générateur ?
yield met le générateur en pause et renvoie une valeur, mais la fonction peut reprendre où elle s'était arrêtée au prochain next(). return, au contraire, termine définitivement le générateur : il passe done à true et plus aucune valeur ne sort. Tu peux faire autant de yield que tu veux, mais un seul return a vraiment du sens.
Quand utiliser un générateur plutôt qu'un tableau ?
Quand la séquence est infinie, coûteuse à calculer, ou que tu n'as besoin que de quelques valeurs. Un générateur produit les éléments un par un à la demande, ce qui te permet de représenter un flux sans fin d'identifiants ou des résultats d'API paginés sans tout charger en mémoire. Si tu as déjà un petit tableau fixe, reste sur le tableau.