Cours JavaScript et TypeScript · Module 5: JavaScript avancé
Les itérateurs et le protocole itérable
Dans cette leçon8
Dans le laboratoire génétique de Jurassic Park, le Dr Henry Wu passe en revue d'immenses ensembles de données ADN de dinosaures. "Il nous faut un moyen de traiter les séquences génétiques élément par élément, sans charger toute la base en mémoire", dit-il. La boucle for...of gère un tableau sans problème, mais fais-y passer un simple objet parc avec ses secteurs, et tu obtiendras un TypeError t'informant que l'objet n'est pas itérable. Heureusement, JavaScript offre les itérateurs et le protocole itérable - des mécanismes qui permettent de contrôler la façon dont on parcourt une collection de données.
Qu'est-ce qu'un itérateur ?
Un itérateur est un objet qui sait accéder aux éléments d'une collection un par un, en suivant sa position actuelle. Un itérateur doit implémenter une méthode next(), qui renvoie un objet avec deux propriétés :
value- la valeur actuelledone-trues'il n'y a plus d'éléments,falsesinon
1// Un itérateur simple - créé à la main
2function createDinoIterator(dinosaurs) {
3 let index = 0;
4
5 return {
6 next() {
7 if (index < dinosaurs.length) {
8 return { value: dinosaurs[index++], done: false };
9 }
10 return { value: undefined, done: true };
11 }
12 };
13}
14
15const raptorIterator = createDinoIterator(["Blue", "Charlie", "Delta", "Echo"]);
16
17console.log(raptorIterator.next()); // { value: "Blue", done: false }
18console.log(raptorIterator.next()); // { value: "Charlie", done: false }
19console.log(raptorIterator.next()); // { value: "Delta", done: false }
20console.log(raptorIterator.next()); // { value: "Echo", done: false }
21console.log(raptorIterator.next()); // { value: undefined, done: true }La variable index vit dans une closure et retient la position entre les appels à next(). Après le dernier raptor, l'itérateur signale done: true et répondra toujours ainsi - il ne recommence pas depuis le début. Le tableau transmis n'a pas changé d'un iota.
Le protocole itérable (Symbol.iterator)
Pour qu'un objet puisse être utilisé dans une boucle for...of, il doit implémenter le protocole itérable - c'est-à-dire posséder une méthode [Symbol.iterator]() qui renvoie un itérateur. De nombreux types intégrés l'implémentent déjà : Array, String, Map, Set.
1// Les tableaux sont itérables
2const species = ["T-Rex", "Velociraptor", "Triceratops"];
3
4for (const dino of species) {
5 console.log(dino);
6}
7// T-Rex
8// Velociraptor
9// Triceratops
10
11// Les chaînes sont aussi itérables
12for (const char of "DINO") {
13 console.log(char); // D, I, N, O
14}
15
16// On peut récupérer manuellement l'itérateur d'un tableau
17const iterator = species[Symbol.iterator]();
18console.log(iterator.next()); // { value: "T-Rex", done: false }
19console.log(iterator.next()); // { value: "Velociraptor", done: false }En coulisses, la boucle for...of fait ce que montrent les dernières lignes : elle récupère l'itérateur via Symbol.iterator et appelle next() jusqu'à voir done: true. Symbol.iterator est un symbole intégré, c'est-à-dire une clé unique que tu ne confondras pas avec un nom de propriété ordinaire. Un littéral d'objet ne possède pas cette méthode, d'où l'erreur de l'introduction.
Créer son propre objet itérable
On peut rendre n'importe quel objet itérable - il suffit de définir la méthode [Symbol.iterator](). Notre parc parcourra tous les secteurs, en rendant un dinosaure à la fois :
1// Jurassic Park comme objet itérable
2const jurassicPark = {
3 name: "Jurassic Park",
4 sectors: [
5 { id: "A", dinosaurs: ["Rexy", "Blue"] },
6 { id: "B", dinosaurs: ["Spike", "Trike"] },
7 { id: "C", dinosaurs: ["Pteranodon", "Mosasaurus"] }
8 ],
9
10 // Implémentation du protocole itérable
11 [Symbol.iterator]() {
12 let sectorIndex = 0;
13 let dinoIndex = 0;
14 const sectors = this.sectors;
15
16 return {
17 next() {
18 // On parcourt tous les dinosaures de tous les secteurs
19 while (sectorIndex < sectors.length) {
20 const sector = sectors[sectorIndex];
21 if (dinoIndex < sector.dinosaurs.length) {
22 const value = {
23 sector: sector.id,
24 dinosaur: sector.dinosaurs[dinoIndex]
25 };
26 dinoIndex++;
27 return { value, done: false };
28 }
29 sectorIndex++;
30 dinoIndex = 0;
31 }
32 return { value: undefined, done: true };
33 }
34 };
35 }
36};
37
38// Maintenant, on peut itérer sur le parc !
39for (const entry of jurassicPark) {
40 console.log(`Secteur ${entry.sector} : ${entry.dinosaur}`);
41}
42// Secteur A : Rexy
43// Secteur A : Blue
44// Secteur B : Spike
45// Secteur B : Trike
46// Secteur C : Pteranodon
47// Secteur C : Mosasaurus
48
49// L'opérateur spread et la déstructuration fonctionnent aussi
50const allDinos = [...jurassicPark];
51console.log(allDinos.length); // 6Les crochets autour du nom de la méthode indiquent que la clé est un symbole, et non un texte. La boucle while dans next() passe au secteur suivant quand il n'y a plus de dinosaures dans le secteur courant, donc de l'extérieur on voit une seule liste plate. Les données de sectors restent intactes - l'itérateur se contente de les lire.
Les générateurs comme itérateurs
Écrire next() à la main est fastidieux. Les générateurs sont des fonctions spéciales marquées d'un astérisque (function*), qui simplifient la création d'itérateurs. Ils utilisent le mot-clé yield pour "produire" les valeurs successives :
1// Générateur de séquence d'ADN
2function* dnaSequenceGenerator(sequence) {
3 for (const nucleotide of sequence) {
4 yield nucleotide;
5 }
6}
7
8const dna = dnaSequenceGenerator("ATCGATCG");
9console.log(dna.next()); // { value: "A", done: false }
10console.log(dna.next()); // { value: "T", done: false }
11console.log(dna.next()); // { value: "C", done: false }
12
13// Les générateurs sont itérables - ils fonctionnent avec for...of
14for (const nucleotide of dnaSequenceGenerator("GCTA")) {
15 console.log(nucleotide); // G, C, T, A
16}Appeler un générateur n'exécute pas encore son corps, cela renvoie seulement un objet générateur. Chaque next() exécute le code jusqu'au yield suivant et le suspend là, en conservant son état. L'astérisque se place à côté du mot function, pas du nom - l'écriture function myGen*() est une erreur de syntaxe.
Un générateur avec de la logique
Un générateur peut produire des valeurs sans fin, car il ne calcule chacune qu'au moment où quelqu'un la demande. C'est ainsi que fonctionne une machine à attribuer des identifiants :
1// Générateur d'identifiants de dinosaures
2function* dinoIdGenerator(prefix, startFrom = 1) {
3 let id = startFrom;
4 while (true) {
5 yield `${prefix}-${String(id).padStart(3, "0")}`;
6 id++;
7 }
8}
9
10const raptorIds = dinoIdGenerator("RAPTOR");
11console.log(raptorIds.next().value); // "RAPTOR-001"
12console.log(raptorIds.next().value); // "RAPTOR-002"
13console.log(raptorIds.next().value); // "RAPTOR-003"
14
15// Générateur infini - mais on ne prend que ce dont on a besoin
16const trexIds = dinoIdGenerator("TREX", 100);
17const firstFive = [];
18for (let i = 0; i < 5; i++) {
19 firstFive.push(trexIds.next().value);
20}
21console.log(firstFive);
22// ["TREX-100", "TREX-101", "TREX-102", "TREX-103", "TREX-104"]La boucle while (true) ne bloque pas le programme, car le générateur travaille paresseusement : il s'arrête sur yield et attend le next() suivant. Ne décompresse cependant jamais un générateur infini avec le spread - un tel appel ne se termine jamais. La méthode padStart(3, "0") complète le numéro avec des zéros jusqu'à trois chiffres.
Un exemple pratique : la pagination des données
Les itérateurs conviennent parfaitement à la pagination de grands ensembles de données, car chaque page n'est créée qu'à la demande :
1// Générateur de pages de données sur les dinosaures
2function* paginateDinosaurs(dinosaurs, pageSize) {
3 for (let i = 0; i < dinosaurs.length; i += pageSize) {
4 yield {
5 page: Math.floor(i / pageSize) + 1,
6 data: dinosaurs.slice(i, i + pageSize),
7 hasMore: i + pageSize < dinosaurs.length
8 };
9 }
10}
11
12const allDinosaurs = [
13 "T-Rex", "Velociraptor", "Triceratops", "Stegosaurus",
14 "Brachiosaurus", "Pteranodon", "Mosasaurus", "Dilophosaurus",
15 "Gallimimus", "Parasaurolophus"
16];
17
18const pages = paginateDinosaurs(allDinosaurs, 3);
19
20console.log(pages.next().value);
21// { page: 1, data: ["T-Rex", "Velociraptor", "Triceratops"], hasMore: true }
22
23console.log(pages.next().value);
24// { page: 2, data: ["Stegosaurus", "Brachiosaurus", "Pteranodon"], hasMore: true }
25
26console.log(pages.next().value);
27// { page: 3, data: ["Mosasaurus", "Dilophosaurus", "Gallimimus"], hasMore: true }
28
29console.log(pages.next().value);
30// { page: 4, data: ["Parasaurolophus"], hasMore: false }Chaque next() rend une portion de trois espèces et le drapeau hasMore, qui indique s'il faut demander la suivante. La dernière page n'a qu'un élément, car slice() ne dépasse pas la fin du tableau.
Un objet itérable avec un générateur
Les générateurs simplifient la création d'un objet itérable. Au lieu d'assembler à la main un objet doté d'une méthode next(), il suffit de marquer d'un astérisque la méthode [Symbol.iterator] :
1class DinosaurEnclosure {
2 constructor(name) {
3 this.name = name;
4 this.dinosaurs = [];
5 }
6
7 add(dinosaur) {
8 this.dinosaurs.push(dinosaur);
9 }
10
11 // Un générateur comme Symbol.iterator - bien plus simple !
12 *[Symbol.iterator]() {
13 for (const dino of this.dinosaurs) {
14 yield dino;
15 }
16 }
17}
18
19const paddock = new DinosaurEnclosure("Raptor Paddock");
20paddock.add({ name: "Blue", species: "Velociraptor" });
21paddock.add({ name: "Charlie", species: "Velociraptor" });
22paddock.add({ name: "Delta", species: "Velociraptor" });
23
24for (const raptor of paddock) {
25 console.log(`Raptor : ${raptor.name}`);
26}
27// Raptor : Blue
28// Raptor : Charlie
29// Raptor : Delta
30
31// L'opérateur spread fonctionne automatiquement
32const names = [...paddock].map(r => r.name);
33console.log(names); // ["Blue", "Charlie", "Delta"]Compare avec le parc de la section précédente : là-bas une quinzaine de lignes avec des indices, ici trois. L'objet générateur satisfait à lui seul les deux protocoles à la fois, la classe fonctionne donc immédiatement avec for...of et avec le spread.
Déstructuration et spread
Le spread et la déstructuration de tableaux utilisent eux aussi le protocole itérable - tous deux sont entrés dans le langage avec ES6 (ES2015), tout comme la déstructuration d'objets :
1const raptors = ["Blue", "Charlie", "Delta", "Echo"];
2
3// Déstructuration de tableau - valeurs successives de l'itérateur
4const [first, second, third] = raptors;
5console.log(first, second, third); // Blue Charlie Delta
6
7// Spread - fusion de tableaux
8const newcomers = ["Fox", "Ghost"];
9const allRaptors = [...raptors, ...newcomers];
10console.log(allRaptors.length); // 6
11
12// Déstructuration et spread d'objet - sans itérateurs
13const raptor = { name: "Blue", age: 4, isAlpha: true };
14const { name, age, isAlpha } = raptor;
15const olderBlue = { ...raptor, age: 5 };
16console.log(name, age, isAlpha, olderBlue.age); // Blue 4 true 5La déstructuration de tableau prend les valeurs une à une dans l'itérateur, c'est pourquoi elle fonctionnera aussi avec le parc ou un générateur. Pour les objets, c'est une autre histoire : la déstructuration d'objet va chercher les propriétés par leur nom, et le spread d'objet, ajouté dans ES2018, copie ses propres champs - aucun des deux n'utilise Symbol.iterator. La copie est superficielle, et le raptor d'origine a toujours 4 ans.
Résumé
Le Dr Wu résume : "Les itérateurs sont comme des sondes génétiques spécialisées - ils permettent de parcourir les données de façon séquentielle, pas à pas :"
- Itérateur - un objet doté d'une méthode
next()qui renvoie{ value, done } - Symbol.iterator - la méthode qui rend un objet itérable (il fonctionne avec
for...of) - Générateurs (
function*+yield) - simplifient la création d'itérateurs - Protocole itérable - la convention selon laquelle un objet peut être itéré, utilisée par
for...of, le spread (...) et la déstructuration
Dans le code de tous les jours, je te recommande le générateur plutôt qu'un next() écrit à la main - il est plus court, et on s'y trompe plus difficilement. Le spread reviendra dans cette location avec le motif Builder, où la méthode build() rend une copie de l'objet. Dans le laboratoire ci-dessous, tu construiras la classe DinosaurEnclosure avec un générateur comme Symbol.iterator.
Retiens bien : un itérateur est une sonde qui lit les échantillons d'ADN un par un, et Symbol.iterator est la prise dans laquelle s'enclenche chaque boucle for...of.
Code de cette leçon : index.js
1// Iterateurs et protocole d'iterabilite
2// Jurassic Park - Laboratoire genetique
3
4console.log("=== ITERATEUR MANUEL ===");
5
6// Creation d'un iterateur simple
7function createDinoIterator(dinosaurs) {
8 let index = 0;
9 return {
10 next() {
11 if (index < dinosaurs.length) {
12 return { value: dinosaurs[index++], done: false };
13 }
14 return { value: undefined, done: true };
15 }
16 };
17}
18
19const raptorIterator = createDinoIterator(["Blue", "Charlie", "Delta", "Echo"]);
20console.log(raptorIterator.next()); // { value: "Blue", done: false }
21console.log(raptorIterator.next()); // { value: "Charlie", done: false }
22console.log(raptorIterator.next()); // { value: "Delta", done: false }
23console.log(raptorIterator.next()); // { value: "Echo", done: false }
24console.log(raptorIterator.next()); // { value: undefined, done: true }
25
26console.log("\n=== SYMBOL.ITERATOR ===");
27
28// Objet iterable avec Symbol.iterator
29const jurassicPark = {
30 sectors: [
31 { id: "A", dinosaurs: ["Rexy", "Blue"] },
32 { id: "B", dinosaurs: ["Spike", "Trike"] },
33 ],
34
35 [Symbol.iterator]() {
36 let sectorIndex = 0;
37 let dinoIndex = 0;
38 const sectors = this.sectors;
39
40 return {
41 next() {
42 while (sectorIndex < sectors.length) {
43 const sector = sectors[sectorIndex];
44 if (dinoIndex < sector.dinosaurs.length) {
45 const value = {
46 sector: sector.id,
47 dinosaur: sector.dinosaurs[dinoIndex]
48 };
49 dinoIndex++;
50 return { value, done: false };
51 }
52 sectorIndex++;
53 dinoIndex = 0;
54 }
55 return { value: undefined, done: true };
56 }
57 };
58 }
59};
60
61// for...of fonctionne grace a Symbol.iterator
62for (const entry of jurassicPark) {
63 console.log(`Secteur ${entry.sector} : ${entry.dinosaur}`);
64}
65
66// L'operateur spread fonctionne aussi
67const allDinos = [...jurassicPark];
68console.log("Tous les dinosaures :", allDinos);
69
70console.log("\n=== GENERATEURS ===");
71
72// Generateur - une facon simplifiee de creer des iterateurs
73function* dinoIdGenerator(prefix, startFrom = 1) {
74 let id = startFrom;
75 while (true) {
76 yield `${prefix}-${String(id).padStart(3, "0")}`;
77 id++;
78 }
79}
80
81const raptorIds = dinoIdGenerator("RAPTOR");
82console.log(raptorIds.next().value); // "RAPTOR-001"
83console.log(raptorIds.next().value); // "RAPTOR-002"
84console.log(raptorIds.next().value); // "RAPTOR-003"
85
86// Generateur de pagination
87function* paginateDinosaurs(dinosaurs, pageSize) {
88 for (let i = 0; i < dinosaurs.length; i += pageSize) {
89 yield {
90 page: Math.floor(i / pageSize) + 1,
91 data: dinosaurs.slice(i, i + pageSize),
92 hasMore: i + pageSize < dinosaurs.length
93 };
94 }
95}
96
97const allSpecies = ["T-Rex", "Velociraptor", "Triceratops", "Stegosaurus",
98 "Brachiosaurus", "Pteranodon", "Mosasaurus", "Dilophosaurus"];
99
100const pages = paginateDinosaurs(allSpecies, 3);
101console.log("\nPagination :");
102console.log(pages.next().value);
103console.log(pages.next().value);
104console.log(pages.next().value);
105
106// TODO : cree une classe DinosaurEnclosure avec un generateur comme Symbol.iterator
107// La classe doit avoir une methode add() et etre iterable
108console.log("\n=== Exercice ===");
109console.log("Cree la classe DinosaurEnclosure - voir le TODO dans le code");Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel objet la méthode next() d'un itérateur doit-elle renvoyer en JavaScript ?
2. Que doit implémenter un objet pour être utilisable dans une boucle for...of ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Utilise la déstructuration de tableau pour affecter les trois premiers vélociraptors à des variables.
- Éditeur de code
Extrais name, age et isAlpha de l'objet raptor à l'aide de la déstructuration.
- Éditeur de code
Fusionne deux groupes de vélociraptors à l'aide de l'opérateur de décomposition.
- Tri vertical
Ordonne les modèles de conception d'objets, du plus simple au plus complexe :
- Éditeur de code
Crée les méthodes hunt(), communicate() et followPack() dans l'objet raptor.
- Éditeur de code
Crée un objet paddock contenant un tableau raptors et des méthodes de gestion.
- Tri vertical
Ordonne chronologiquement les fonctionnalités modernes de JavaScript selon le standard :
- Tri vertical
Ordonne les étapes de gestion de la mémoire pour les objets et les tableaux :
- Éditeur de code
Crée une variable qui stocke une espèce de dinosaure
- Tri vertical
Ordonne le processus de choix d'une structure de données :
- Éditeur de code
Écris une fonction qui calcule l'âge d'un fossile