Cours JavaScript et TypeScript · Module 4: JavaScript asynchrone
Itérateurs et générateurs
Dans cette leçon6
Le génome d'un dinosaure, ce sont des milliards de paires de bases. Si le laboratoire de Jurassic Park chargeait toute la séquence d'un coup, les ordinateurs planteraient avant que quiconque ait lu le premier gène. Les scientifiques analysent donc l'ADN fragment par fragment, à la demande. Les itérateurs et les générateurs de JavaScript fonctionnent de la même façon : ils permettent de traiter les données élément par élément, de manière paresseuse et économe. "C'est la chaîne de production du laboratoire", dit le Dr Rex. "Tu reçois l'échantillon suivant exactement au moment où tu en as besoin."
Le protocole d'itération
Tout objet qui possède une méthode [Symbol.iterator]() est itérable et peut être utilisé dans une boucle for...of. Les tableaux et les chaînes la possèdent de naissance :
1// Tableaux, chaînes, Map, Set - sont itérables par nature
2for (const char of 'ATCG') {
3 console.log(char); // A, T, C, G
4}
5
6for (const item of [1, 2, 3]) {
7 console.log(item); // 1, 2, 3
8}En coulisses, for...of appelle [Symbol.iterator]() et obtient un itérateur, c'est-à-dire un objet doté d'une méthode next(). Tu peux l'appeler à la main :
1const it = ['T-Rex', 'Raptor'][Symbol.iterator]();
2console.log(it.next()); // { value: 'T-Rex', done: false }
3console.log(it.next()); // { value: 'Raptor', done: false }
4console.log(it.next()); // { value: undefined, done: true }Chaque appel renvoie un objet avec les champs value et done. La boucle s'arrête quand done vaut true, et le value de ce dernier résultat est alors ignoré.
Créer son propre itérateur
Un objet ordinaire n'est pas itérable. Pour pouvoir parcourir un enclos avec une boucle, nous lui ajoutons [Symbol.iterator](), qui renvoie un objet doté de next() :
1const dinoEnclosure = {
2 dinosaurs: ['Rex', 'Blue', 'Delta', 'Echo'],
3
4 [Symbol.iterator]() {
5 let index = 0;
6 const dinos = this.dinosaurs;
7
8 return {
9 next() {
10 if (index < dinos.length) {
11 return { value: dinos[index++], done: false };
12 }
13 return { value: undefined, done: true };
14 }
15 };
16 }
17};
18
19// Maintenant, on peut utiliser for...of !
20for (const dino of dinoEnclosure) {
21 console.log('Dinosaure :', dino);
22}
23
24// Et l'opérateur spread
25const allDinos = [...dinoEnclosure];
26console.log(allDinos); // ['Rex', 'Blue', 'Delta', 'Echo']Le compteur index vit dans une closure, donc chaque nouvelle boucle repart de zéro. Le spread fonctionne immédiatement, car il utilise le même protocole que for...of.
for...of contre for...in
Ces deux boucles ont des noms semblables, mais des rôles totalement différents. Compare-les sur un objet et sur un tableau :
1const park = { name: 'Jurassic', size: 'large', open: false };
2const species = ['T-Rex', 'Velociraptor', 'Triceratops'];
3
4// for...in - parcourt les CLÉS (des objets et des tableaux)
5for (const key in park) {
6 console.log(key); // 'name', 'size', 'open'
7}
8
9for (const index in species) {
10 console.log(index); // '0', '1', '2' (des chaînes !)
11}
12
13// for...of - parcourt les VALEURS (des itérables)
14for (const animal of species) {
15 console.log(animal); // 'T-Rex', 'Velociraptor', 'Triceratops'
16}
17
18// for...of NE fonctionne PAS sur les objets ordinaires !
19// for (const val of park) {} // TypeError!
20// Mais il fonctionne sur Object.entries() :
21for (const [key, val] of Object.entries(park)) {
22 console.log(key, val);
23}for...in renvoie les clés sous forme de chaînes et visite aussi les propriétés héritées du prototype. Mon conseil : parcours les tableaux avec for...of, et les objets avec for...of et Object.entries().
Les générateurs (function*)
Écrire next() à la main est fastidieux. Un générateur tient cette comptabilité pour toi : c'est une fonction marquée d'un astérisque, function*, qui sait suspendre son exécution sur yield et la reprendre plus tard :
1function* dinoGenerator() {
2 console.log('Génération du premier dinosaure...');
3 yield 'T-Rex';
4
5 console.log('Génération du deuxième...');
6 yield 'Velociraptor';
7
8 console.log('Génération du troisième...');
9 yield 'Triceratops';
10
11 console.log('Génération terminée !');
12}
13
14const gen = dinoGenerator();
15
16console.log(gen.next()); // { value: 'T-Rex', done: false }
17console.log(gen.next()); // { value: 'Velociraptor', done: false }
18console.log(gen.next()); // { value: 'Triceratops', done: false }
19console.log(gen.next()); // { value: undefined, done: true }Le simple appel dinoGenerator() n'affiche rien, il renvoie seulement un objet générateur. Le code ne démarre qu'au premier next() et s'exécute jusqu'au yield suivant, donc chaque message apparaît juste avant son résultat. Un générateur peut aussi se terminer par return :
1function* dinos() {
2 yield 'T-Rex';
3 yield 'Raptor';
4 return 'done';
5}
6
7const herd = dinos();
8console.log(herd.next()); // { value: 'T-Rex', done: false }
9console.log(herd.next()); // { value: 'Raptor', done: false }
10console.log(herd.next()); // { value: 'done', done: true }
11console.log([...dinos()]); // ['T-Rex', 'Raptor'] - sans 'done'La valeur de return arrive avec done: true, c'est pourquoi for...of et le spread l'ignorent.
Le générateur infini
Un générateur peut ne jamais se terminer, car il ne calcule la valeur suivante qu'à la demande :
1function* idGenerator(prefix) {
2 let id = 1;
3 while (true) {
4 yield prefix + '-' + String(id).padStart(3, '0');
5 id++;
6 }
7}
8
9const dinoIds = idGenerator('DINO');
10console.log(dinoIds.next().value); // 'DINO-001'
11console.log(dinoIds.next().value); // 'DINO-002'
12console.log(dinoIds.next().value); // 'DINO-003'
13// Ne se termine jamais - génère à la demande !La boucle while (true) ne bloque pas le programme, car l'exécution s'arrête à chaque yield. Ne décompresse cependant jamais un tel générateur avec le spread : [...dinoIds] essaierait de rassembler une infinité de valeurs.
yield* - déléguer à un autre générateur
yield* transmet une à une toutes les valeurs d'un autre générateur ou d'un objet itérable, on peut donc assembler de petits générateurs en de plus grands :
1function* carnivores() {
2 yield 'T-Rex';
3 yield 'Velociraptor';
4}
5
6function* herbivores() {
7 yield 'Triceratops';
8 yield 'Brachiosaurus';
9}
10
11function* allDinosaurs() {
12 yield* carnivores();
13 yield* herbivores();
14}
15
16for (const dino of allDinosaurs()) {
17 console.log(dino);
18}
19// T-Rex, Velociraptor, Triceratops, BrachiosaurusComme yield* fonctionne aussi sur les tableaux, tu peux réduire l'itérateur maison de l'enclos à une seule ligne : *[Symbol.iterator]() { yield* this.dinosaurs; }. C'est ainsi que je te recommande de l'écrire.
Les générateurs asynchrones
Les données d'un serveur arrivent page par page, et chacune demande un await. Un générateur asynchrone combine async et function*, et ses résultats sont reçus par la boucle for await...of :
1async function* fetchDinoPages(apiUrl) {
2 let page = 1;
3 let hasMore = true;
4
5 while (hasMore) {
6 const response = await fetch(apiUrl + '?page=' + page);
7 const data = await response.json();
8
9 yield data.results;
10
11 hasMore = data.hasNextPage;
12 page++;
13 }
14}
15
16// Utilisation avec for await...of
17async function loadAllDinos() {
18 for await (const batch of fetchDinoPages('/api/dinosaurs')) {
19 console.log('Groupe chargé :', batch.length, 'dinosaures');
20 }
21}La page suivante n'est chargée que lorsque la boucle demande la portion suivante. Si personne ne traite les résultats, aucune requête supplémentaire ne part vers le serveur.
Les usages pratiques
Le premier classique est l'évaluation paresseuse : la suite de Fibonacci est infinie, et nous n'en prenons que ce dont nous avons besoin :
1// 1. Évaluation paresseuse - traitement à la demande
2function* fibonacci() {
3 let a = 0, b = 1;
4 while (true) {
5 yield a;
6 [a, b] = [b, a + b];
7 }
8}
9
10// Ne prendre que les 10 premiers
11const fib = fibonacci();
12const first10 = [];
13for (let i = 0; i < 10; i++) {
14 first10.push(fib.next().value);
15}
16console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]La ligne [a, b] = [b, a + b] est l'échange de valeurs par déstructuration que tu connais depuis le module 2. Le second classique est la pagination, c'est-à-dire la fourniture de données par portions :
1// 2. Pagination - charger les pages à la demande
2function* paginate(items, pageSize) {
3 for (let i = 0; i < items.length; i += pageSize) {
4 yield items.slice(i, i + pageSize);
5 }
6}
7
8const allSpecies = ['A','B','C','D','E','F','G','H'];
9const pages = paginate(allSpecies, 3);
10console.log(pages.next().value); // ['A', 'B', 'C']
11console.log(pages.next().value); // ['D', 'E', 'F']
12console.log(pages.next().value); // ['G', 'H']La dernière page n'a que deux éléments, car slice s'arrête à la fin du tableau. Ce même schéma de boucle avec un pas et yield suffit pour écrire ton propre range(start, end, step).
Pratique
Dans le bac à sable t'attendent l'itérateur maison de l'enclos et des générateurs à t'exercer. Appelle next() à la main et prédis quand done: true apparaîtra.
Code de cette leçon : index.js
1// === ITERATORS ET GENERATORS ===
2
3// 1. Custom iterator
4const enclosure = {
5 dinosaurs: ['Rex', 'Blue', 'Delta', 'Echo'],
6 [Symbol.iterator]() {
7 let i = 0;
8 const dinos = this.dinosaurs;
9 return {
10 next() {
11 return i < dinos.length
12 ? { value: dinos[i++], done: false }
13 : { done: true };
14 }
15 };
16 }
17};
18
19console.log('=== Iterator ===');
20for (const dino of enclosure) {
21 console.log('Dino:', dino);
22}
23console.log('Spread:', [...enclosure]);
24
25// 2. for...of vs for...in
26console.log('\n=== for...of vs for...in ===');
27const species = ['T-Rex', 'Raptor', 'Triceratops'];
28console.log('for...in (clés):');
29for (const i in species) console.log(' ', i, typeof i);
30console.log('for...of (values):');
31for (const s of species) console.log(' ', s);
32
33// 3. Generator
34function* dinoGen() {
35 yield 'T-Rex';
36 yield 'Velociraptor';
37 yield 'Triceratops';
38}
39console.log('\n=== Generator ===');
40const gen = dinoGen();
41console.log(gen.next()); // { value: 'T-Rex', done: false }
42console.log(gen.next()); // { value: 'Velociraptor', done: false }
43console.log(gen.next()); // { value: 'Triceratops', done: false }
44console.log(gen.next()); // { done: true }
45
46// 4. Infinite generator
47function* idGen(prefix) {
48 let id = 1;
49 while (true) {
50 yield prefix + '-' + String(id++).padStart(3, '0');
51 }
52}
53console.log('\n=== ID Generator ===');
54const ids = idGen('DINO');
55console.log(ids.next().value); // DINO-001
56console.log(ids.next().value); // DINO-002
57console.log(ids.next().value); // DINO-003
58
59// 5. yield* - délégation
60function* carnivores() { yield 'T-Rex'; yield 'Raptor'; }
61function* herbivores() { yield 'Triceratops'; yield 'Brachio'; }
62function* allDinos() { yield* carnivores(); yield* herbivores(); }
63console.log('\n=== yield* ===');
64console.log([...allDinos()]);
65
66// 6. Pagination avec un generator
67function* paginate(items, size) {
68 for (let i = 0; i < items.length; i += size) {
69 yield items.slice(i, i + size);
70 }
71}
72console.log('\n=== Pagination ===');
73const pages = paginate(['A','B','C','D','E','F','G'], 3);
74console.log('Page 1:', pages.next().value);
75console.log('Page 2:', pages.next().value);
76console.log('Page 3:', pages.next().value);Retiens bien : un générateur est le tapis roulant du laboratoire, qui ne te tend l'échantillon suivant qu'au moment où tu avances la main pour le prendre.
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. Quelle méthode un objet doit-il implémenter pour être itérable (utilisable dans for...of) ?
2. Comment marque-t-on une fonction comme générateur en JavaScript ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Ordonne les éléments pour créer un générateur async :
- Éditeur de code
Implémente [Symbol.iterator], un générateur d'ID et un générateur de pagination
- Tri vertical
Ordonne les étapes d'un itérateur, du premier appel à next() jusqu'à la fin :
- Clique dans l'ordre
Ordonne les éléments pour créer un générateur simple :
- Éditeur de code
Implémente Fibonacci, yield* pour composer des générateurs, et un générateur range(start, end, step)
- Tri vertical
Ordonne les éléments de l'implémentation d'un protocole itérateur :
- Clique dans l'ordre
Ordonne les éléments d'un générateur avec plusieurs yields :