L'itération
Le protocole d'itération de JavaScript : parcourir n'importe quelle collection avec for..of, le spread et les itérables.
Un programme passe le plus clair de son temps à traiter des données et à prendre des décisions à partir d'elles. Or la façon dont on parcourt ces données — ligne par ligne, élément par élément — pèse lourd sur la lisibilité du code. Kyle Simpson ouvre son exploration des « racines » de JavaScript par un mécanisme qu'on utilise tous les jours sans toujours le nommer : l'itération.
L'idée centrale est ancienne et tient en une phrase. Plutôt que d'avaler une collection entière d'un coup, il est souvent plus naturel de la traiter progressivement : on prend le premier morceau, puis le suivant, et ainsi de suite. Ce que JavaScript a apporté en ES6, ce n'est pas l'idée, mais un protocole standardisé pour la mettre en œuvre — de sorte que la même boucle fonctionne partout. C'est tout l'objet de ce chapitre.
Le patron itérateur
Le patron itérateur (iterator pattern) existe depuis des décennies. Il propose une approche « standardisée » pour consommer les données d'une source un morceau à la fois, sans avoir à connaître la structure interne de cette source.
L'image que prend Simpson est parlante. Imaginez le résultat d'une requête SELECT dans une base relationnelle, organisé en lignes. Si la requête ne renvoie qu'une ou deux lignes, vous pouvez tout traiter d'un coup. Mais si elle en renvoie 100, 1 000, ou davantage, vous avez besoin d'un traitement itératif — typiquement, une boucle.
Le patron définit pour cela une structure appelée itérateur (iterator) : un objet qui garde une référence vers la source de données sous-jacente et expose une méthode next(). Chaque appel à next() renvoie le morceau suivant. Comme on ne sait pas toujours combien de morceaux il reste, le patron signale la fin par une valeur spéciale une fois qu'on a dépassé le dernier élément.
Note
L'intérêt du patron n'est pas la mécanique de next() en elle-même, mais l'uniformité. Si chaque structure de données inventait sa propre manière d'être parcourue, le code serait illisible. Un protocole commun rend les programmes plus reconnaissables et plus faciles à comprendre.
Le protocole d'itération
Après des années d'efforts de la communauté autour de techniques d'itération communes, ES6 a standardisé un protocole précis directement dans le langage.
Ce protocole définit une méthode next() dont le retour est un objet appelé résultat d'itération (iterator result). Cet objet possède deux propriétés : value (la valeur courante) et done (un booléen). done reste false tant que le parcours de la source n'est pas terminé, et passe à true une fois la source épuisée.
On peut écrire un itérateur entièrement à la main pour démystifier ce qui se passe :
// Un itérateur "maison" : un objet avec une méthode next().
function compteurJusqua(max) {
let n = 0;
return {
next() {
n += 1;
if (n <= max) return { value: n, done: false };
return { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
},
};
}
var it = compteurJusqua(3);
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true } Rien de magique : un objet, une méthode, un petit contrat sur la forme de l'objet renvoyé. Tout ce qui respecte ce contrat est un itérateur valide.
Consommer un itérateur
Une fois le protocole en place, on pourrait consommer une source valeur par valeur en appelant next() et en vérifiant done à chaque tour pour décider de s'arrêter. C'est possible, mais fastidieux et manuel. ES6 a donc fourni plusieurs mécanismes — syntaxe et API — pour consommer ces itérateurs proprement.
La boucle for..of
Le premier mécanisme est la boucle for..of. Elle pilote next() pour vous, récupère chaque value et s'arrête toute seule quand done devient true.
// `it` est un itérateur d'une source quelconque.
var it = compteurJusqua(3);
for (let val of it) {
console.log(`Valeur : ${val}`);
}
// Valeur : 1
// Valeur : 2
// Valeur : 3 Comparée à la version manuelle (appeler next(), tester done, extraire value, recommencer), la boucle for..of est nettement plus lisible. C'est tout l'intérêt d'avoir un protocole partagé : la syntaxe fait le travail répétitif à votre place.
L'opérateur de décomposition ...
Le second mécanisme courant est l'opérateur .... Il a deux formes symétriques : la décomposition/propagation (spread) et le rassemblement (rest, que Simpson préfère appeler gather). C'est la forme spread qui consomme un itérateur.
Pour étaler un itérateur, il faut une cible où le déverser. JavaScript en offre deux : un tableau ou la liste d'arguments d'un appel de fonction.
// Étaler un itérateur dans un tableau :
// chaque valeur occupe une position du tableau.
var vals = [...it];
// Étaler un itérateur dans un appel de fonction :
// chaque valeur occupe une position d'argument.
doSomethingUseful(...it); Dans les deux cas, la forme spread de ... suit exactement le même protocole de consommation que for..of : elle tire toutes les valeurs disponibles de l'itérateur et les place (les « étale ») dans le contexte d'accueil — tableau ou liste d'arguments.
Astuce
Retenez l'unité du mécanisme : for..of, [...x] et f(...x) ne sont pas trois fonctionnalités séparées. Ce sont trois consommateurs du même protocole. Comprendre l'un, c'est comprendre les trois.
Les itérables
Voici la nuance que Simpson souligne, et qui dénoue beaucoup de confusions. Le protocole de consommation n'est pas, en toute rigueur, défini pour les itérateurs : il est défini pour les itérables (iterables).
Un itérable est une valeur qui peut être parcourue. Quand on lui applique for..of ou ..., le protocole crée automatiquement une instance d'itérateur à partir de l'itérable, puis consomme cette seule instance jusqu'au bout. Conséquence importante : un même itérable peut être consommé plusieurs fois, car chaque consommation fabrique un nouvel itérateur frais.
À retenir
Pourquoi pouvait-on alors passer un itérateur directement à for..of dans les exemples précédents ? Parce qu'un itérateur peut être aussi un itérable de lui-même : c'est précisément l'ajout de [Symbol.iterator]() { return this; } qui le rend itérable — quand on lui demande de produire un itérateur, il se renvoie lui-même. C'est aussi ce que font automatiquement les itérateurs natifs et les générateurs. C'est ce qui réconcilie « consommer un itérateur » et « itérer un itérable » — les deux formulations désignent la même opération.
Ce qui est itérable nativement
Alors, où trouve-t-on des itérables ? ES6 a défini les principaux types de collections de JavaScript comme itérables d'origine. Cela inclut les chaînes, les tableaux (Array), les Map, les Set, et d'autres encore. Certaines valeurs renvoyées par les API du langage le sont également.
Un tableau est donc directement parcourable, et copiable par spread :
// Un tableau est itérable.
var arr = [10, 20, 30];
for (let val of arr) {
console.log(`Valeur du tableau : ${val}`);
}
// Valeur du tableau : 10
// Valeur du tableau : 20
// Valeur du tableau : 30
// Copie superficielle via le protocole d'itération.
var arrCopie = [...arr]; // [10, 20, 30] Une chaîne est itérable elle aussi : on peut en étaler les caractères un par un.
var greeting = "Hello world!";
var chars = [...greeting];
chars;
// [ "H", "e", "l", "l", "o", " ",
// "w", "o", "r", "l", "d", "!" ] Le ... se prête aussi naturellement à la concaténation sans muter les sources : [...a, ...b] crée un nouveau tableau qui contient d'abord les éléments de a, puis ceux de b.
Les Map et la déstructuration
Une Map associe une valeur (de n'importe quel type) à une clé qui peut être un objet. Son itération par défaut diffère de celle d'un tableau : elle ne parcourt pas seulement les valeurs, mais les entrées (entries). Une entrée est un tuple — un tableau à deux éléments contenant la clé et la valeur.
On utilise alors la déstructuration de tableau (array destructuring) [cle, valeur] pour décomposer chaque tuple consommé :
// `btn1` et `btn2` sont deux éléments du DOM.
var buttonNames = new Map();
buttonNames.set(btn1, "Bouton 1");
buttonNames.set(btn2, "Bouton 2");
for (let [btn, btnName] of buttonNames) {
btn.addEventListener("click", function onClick() {
console.log(`Clic sur ${btnName}`);
});
} Ici, [btn, btnName] découpe chaque entrée en sa paire clé/valeur : btn1 / "Bouton 1", puis btn2 / "Bouton 2".
Choisir une itération plus précise
Chaque itérable natif expose une itération par défaut qui correspond le plus souvent à l'intuition. Mais on peut aussi demander une itération plus spécifique. La plupart des itérables natifs offrent trois formes : clés seules (keys()), valeurs seules (values()) et entrées (entries()).
Pour ne parcourir que les valeurs de la Map précédente :
for (let btnName of buttonNames.values()) {
console.log(btnName);
}
// Bouton 1
// Bouton 2 Et pour obtenir l'index et la valeur lors du parcours d'un tableau, on utilise entries() :
var arr = [10, 20, 30];
for (let [idx, val] of arr.entries()) {
console.log(`[${idx}]: ${val}`);
}
// [0]: 10
// [1]: 20
// [2]: 30 Note
Pour les objets simples ({}), qui ne sont pas itérables par défaut, le réflexe est Object.entries(obj), Object.keys(obj) ou Object.values(obj) : ces utilitaires renvoient un tableau — donc un itérable — que for..of sait parcourir. C'est le pendant des entries()/keys()/values() des Map et tableaux.
Rendre ses propres structures itérables
L'uniformité du protocole ne s'arrête pas aux types natifs. Vous pouvez faire adhérer vos propres structures de données au protocole d'itération. En l'implémentant, vous activez d'un coup la possibilité de consommer vos données avec for..of et avec ....
L'intérêt est avant tout de lisibilité collective : « standardiser » sur ce protocole, c'est produire du code globalement plus reconnaissable. Un développeur qui voit for..of/[...x] sait immédiatement ce qui se passe, quelle que soit la structure dessous.
Un mot sur les générateurs
Écrire un itérateur à la main, avec son objet { value, done } et sa gestion de done, reste verbeux. C'est précisément pour cela qu'existent les générateurs (generators) : une fonction function* qui produit ses valeurs avec yield et fabrique automatiquement un itérateur conforme au protocole.
// Le générateur produit un itérateur sans `next()` manuel.
function* compteurJusqua(max) {
for (let n = 1; n <= max; n++) {
yield n;
}
}
for (let val of compteurJusqua(3)) {
console.log(val);
}
// 1
// 2
// 3
[...compteurJusqua(3)]; // [1, 2, 3] Comparez ce générateur à l'itérateur manuel du début : même comportement, mais sans gérer value, done ni l'objet de retour. Le langage s'en charge. Les générateurs sont un sujet à part entière de la série You Don't Know JS Yet ; il suffit ici de retenir qu'ils sont le moyen le plus simple de produire un itérateur.
Pourquoi un protocole uniforme est précieux
Reprenons du recul, car c'est le cœur du chapitre. La valeur n'est pas dans tel ou tel mot-clé, mais dans le fait que la même boucle for..of fonctionne partout : sur un tableau, une chaîne, une Map, un Set, le résultat d'une API, ou votre propre classe.
Sans ce protocole, chaque source imposerait sa façon de la parcourir, et il faudrait réapprendre une API à chaque type. Avec lui, un seul geste mental — « je consomme un itérable » — couvre tous les cas. C'est exactement l'esprit que Simpson veut transmettre : chercher le mécanisme commun sous la surface plutôt que d'empiler des recettes isolées.
À retenir
- Le patron itérateur standardise le parcours d'une source un morceau à la fois, sans connaître sa structure interne ; ES6 l'a inscrit dans le langage.
- Le protocole repose sur
next(), qui renvoie un objet{ value, done };donepasse àtrueune fois la source épuisée. - On consomme un itérable avec
for..of, avec le spread dans un tableau ([...x], copie, concaténation) ou dans un appel (f(...x)) — trois faces du même protocole. - Sont itérables nativement les chaînes, tableaux, Map, Set et d'autres ; on choisit l'itération via
keys(),values(),entries()(etObject.entriespour les objets simples), en déstructurant les tuples avec[cle, valeur]. - Un itérateur est un itérable de lui-même : c'est pourquoi on peut le passer directement à
for..of. - On peut rendre ses propres structures itérables ; les générateurs (
function*+yield) sont la façon la plus simple de produire un itérateur conforme.