Itérateurs et générateurs

par 4 contributeurs :

Effectuer des traitements sur chacun des éléments d'une collection est une opération très fréquente. Il existe plusieurs outils natifs dans JavaScript pour parcourir une collection, les boucles for, map(), filter(), les compréhensions de tableau. Les itérateurs et les générateurs font de ce concept d'itération une fonctionnalité principale du langage et permettent d'adapter et de personnaliser le comportement des boucles for...of.

Pour plus de détails sur les mécanismes d'itération, voir les pages suivantes :

Itérateurs

Un itérateur est un objet sachant comment accéder aux éléments d'une collection un par un et qui connait leur position dans la collection. En JavaScript, un itérateur expose une méthode next() qui retourne l'élément suivant dans la séquence. Cette méthode peut lever une excepction de type StopIteration quand la séquence est épuisée.

Une fois créé, un itérateur peut être utilisé explicitement en appelant sa méthode next(), ou implicitement en utilisant les boucles for...in et for each.

Des itérateurs très simples peuvent être créés au moyen de la fonction Iterator():

function makeIterator(array){
    var nextIndex = 0;
    
    return {
       next: function(){
           return nextIndex < array.length ?
               {value: array[nextIndex++], done: false} :
               {done: true};
       }
    }
}

Une fois initialisée, la méthode next() peut être appelée pour accéder aux paires clé-valeur de l'objet initialement passé en argument :

var it = makeIterator(['yo', 'ya']);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done);  // true

Itérables

Un objet est considéré comme itérable s'il définit le comportement qu'il aura lors de l'itération (par exemple les valeurs qui seront utilisées dans une boucle for..of). Certains types natifs, tels qu'Array ou Map, possède un comportement par défaut pour les itérations, d'autres types, comme Object).

Pour qu'un objet soit itérable, un objet doit implémenter la méthode @@iterator, cela signifie que l'objet (ou un des objets de la chaîne de prototypes) doit avoir une propriété  avec la clé Symbol.iterator.

Itérables personnalisés

Il est possible définir ses propres itérables de cette façon :

var monItérable = {}
monItérable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
[...monItérable] // [1, 2, 3]

Itérables natifs

String, Array, TypedArray, Map et Set sont des itérables natifs car les prototypes de chacuns ont tous une méthode Symbol.iterator.

Les éléments de syntaxe utilisant des itérables

Certaines instructions ou expressions utilisent des itérables, par exemple les boucles for...of, l'opérateur de décomposition, yield*, et l'affectation par décomposition.

for(let value of ["a", "b", "c"]){
    console.log(value)
}
// "a"
// "b"
// "c"

[..."abc"] // ["a", "b", "c"]

function* gen(){
  yield* ["a", "b", "c"]
}

gen().next() // { value:"a", done:false }

[a, b, c] = new Set(["a", "b", "c"])
a // "a"

Générateurs

Les itérateurs personnalisés sont un outil utile mais leur création peut s'avérer complexe et il faut maintenir leur état interne. Avec les générateurs, on peut définir une seule fonction qui est un algorithme itératif et qui peut maintenir son état.

Un générateur est un type de fonction spécial qui fonctionne comme une fabrique (factory) d'itérateurs. Une fonction devient un générateur lorsqu'elle contient une ou plusieurs expressions yield et qu'elle utilise la syntaxe function*.

function* idMaker(){
  var index = 0;
  while(true)
    yield index++;
}

var gen = idMaker();

console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ...

Générateurs avancés

Les générateurs calculent les valeurs à fournir à la demande, ce qui leur permet de représenter efficacement des suites complexes à calculer, voire des séries infinies (comme vu dans l'exemple précédent).

La méthode next() accepte également un argument qui pourra être utilisé pour modifier l'état interne du générateur. Une valeur passée à next() sera traitée comme le résultat de la dernière expression yield qui a interrompu le générateur.

Par exemple, on peut avoir un générateur pour la suite de Fibonnaci et utiliser next(x) pour redémarrer la série :

function* fibonacci(){
  var fn1 = 1;
  var fn2 = 1;
  while (true){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    var reset = yield current;
    if (reset){
        fn1 = 1;
        fn2 = 1;
    }
  }
}

var sequence = fibonacci();
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2
console.log(sequence.next().value);     // 3
console.log(sequence.next().value);     // 5
console.log(sequence.next().value);     // 8
console.log(sequence.next().value);     // 13
console.log(sequence.next(true).value); // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2
console.log(sequence.next().value);     // 3
Note : Si on appelle next(undefined), cela sera équivalent à appeler next(). Cependant, appeler la méthode next() d'un générateur qui vient d'être créé avec toute autre valeur que undefined provoquera une exception TypeError.

Il est possible de forcer un générateur à lever une exception en utilisant la méthode throw() en lui passant la valeur de l'exception en argument. Cette exception sera levée depuis l'état actuel du générateur, comme si le yield qui était en attente avait été une instruction throw valeur.

Si le mot-clé yield n'est pas trouvé lors de la levée de l'exception, l'exception sera propagée jusqu'à l'appel de throw(), les appels à next() qui suivent renverront une valeur dont la propriété done sera true.

Les générateurs possèdent une méthode return(valeur) qui permet de renvoyer une valeur donnée et de terminer le générateur.

Les compréhensions de générateurs

Un des désavantages des compréhensions de tableaux est qu'elles entraînent la création d'un nouveau tableau en mémoire. Lorsque l'entrée est un petit tableau, cela ne pose pas problème mais lorsque l'entrée est un tableau important (voire un générateur infini), la création d'un nouveau tableau à partir d'une telle compréhension sera problématique.

Les générateurs permettent de calculer les suites de façon « paresseuse » où chaque élément est généré à la demande. Syntaxiquement, les compréhensions de générateurs sont quasiment équivalentes aux compréhensions de tableaux (elles utilisent des parenthèses à la place des crochets) et au lieu de générer un tableau, elles construisent un générateur qui peut être utilisé paresseusement. D'une certaine façon, elles peuvent être vues comme une syntaxe raccourcie pour créer des générateurs.

Si, par exemple, on a un itérateur it qui itère sur une grande séquence d'entiers et qu'on souhaite créer un nouvel itérateur qui itère sur les doubles de ces entiers, une compréhension de tableaux aurait créer un tableau entier en mémoire avec les valeurs doublées :

var doubles = [i * 2 for (i in it)];

Une compréhension de générateur quant à elle aurait créé un nouvel itérateur qui aurait doublé les valeurs au fur et à mesure qu'on en a besoin :

var it2 = (i * 2 for (i in it));
console.log(it2.next()); // La première valeur de it, doublée
console.log(it2.next()); // La seconde valeur de it, doublée

Lorsqu'une compréhension de générateur est utilisée comme l'argument d'une fonction, les parenthèses utilisées pour l'appel d'une fonction signifient que les parenthèses utilisées pour le générateur peuvent être omises :

var result = faireQuelqueChose(i * 2 for (i in it));

La principale différence entre les deux exemples (tableau vs. générateur) est qu'avec la compréhension de générateur, on ne doit parcourir l'objet qu'une seule fois. Lorsqu'on utilise une compréhension de tableau, l'objet est parcouru pour construire le tableau dans un premier temps puis parcouru à nouveau lors de l'itération.

Pour plus d'informations, voir la page sur les compréhensions de générateurs.

Étiquettes et contributeurs liés au document

Contributeurs à cette page : SphinxKnight, teoli, goofy_bz, n1k0
Dernière mise à jour par : SphinxKnight,
Masquer la barre latérale