Comment utiliser les générateurs et les itérateurs en JavaScript

Comment utiliser les générateurs et les itérateurs en JavaScript

Itérer sur des collections de données à l’aide de boucles traditionnelles peut rapidement devenir fastidieux et lent, en particulier lorsqu’il s’agit de quantités massives de données.

Les générateurs et les itérateurs JavaScript fournissent une solution pour itérer efficacement sur de grandes collections de données. En les utilisant, vous pouvez contrôler le flux d’itération, produire des valeurs une par une, et mettre en pause et reprendre le processus d’itération.

Ici, vous couvrirez les bases et les éléments internes d’un itérateur JavaScript et comment vous pouvez générer un itérateur manuellement et à l’aide d’un générateur.

Itérateurs JavaScript

Un itérateur est un objet JavaScript qui implémente le protocole itérateur. Ces objets le font en ayant une méthode suivante . Cette méthode renvoie un objet qui implémente l’ interface IteratorResult .

L’ interface IteratorResult comprend deux propriétés : done et value . La propriété done est un booléen qui renvoie false si l’itérateur peut produire la valeur suivante dans sa séquence ou true si l’itérateur a terminé sa séquence.

La propriété value est une valeur JavaScript renvoyée par l’itérateur lors de sa séquence. Lorsqu’un itérateur termine sa séquence (when done === true ), cette propriété renvoie undefined .

Comme leur nom l’indique, les itérateurs vous permettent de « itérer » sur des objets JavaScript tels que des tableaux ou des cartes. Ce comportement est possible grâce au protocole itérable.

En JavaScript, le protocole itérable est un moyen standard de définir des objets sur lesquels vous pouvez itérer, comme dans une boucle for…of .

Par exemple:

const fruits = ["Banana", "Mango", "Apple", "Grapes"];

for (const iterator of fruits) {
  console.log(iterator);
}


/*
Banana
Mango
Apple
Grapes
*/

Cet exemple itère sur le tableau fruits en utilisant une boucle for…of . À chaque itération, il enregistre la valeur actuelle dans la console. Ceci est possible car les tableaux sont itérables.

Certains types JavaScript, tels que Arrays, Strings, Sets et Maps, sont des itérables intégrés car ils (ou l’un des objets de leur chaîne de prototypes) implémentent une méthode @@iterator .

D’autres types, tels que les objets, ne sont pas itérables par défaut.

Par exemple:

const iterObject = {
  cars: ["Tesla", "BMW", "Toyota"],
  animals: ["Cat", "Dog", "Hamster"],
  food: ["Burgers", "Pizza", "Pasta"],
};

for (const iterator of iterObject) {
  console.log(iterator);
}


// TypeError: iterObject is not iterable

Cet exemple montre ce qui se passe lorsque vous essayez d’itérer sur un objet qui n’est pas itérable.

Rendre un objet itérable

Pour rendre un objet itérable, vous devez implémenter une méthode Symbol.iterator sur l’objet. Pour devenir itérable, cette méthode doit retourner un objet qui implémente l’ interface IteratorResult .

Les blocs de code ci-dessous fournissent un exemple de la façon de rendre un objet itérable à l’aide de iterObject .

Tout d’abord, ajoutez la méthode Symbol.iterator à iterObject à l’aide d’une déclaration de fonction.

Ainsi:

iterObject[Symbol.iterator] = function () {
  // Subsequent code blocks go here...
}

Ensuite, vous devrez accéder à toutes les clés de l’objet que vous souhaitez rendre itérable. Vous pouvez accéder aux clés à l’aide de la méthode Object.keys , qui renvoie un tableau des propriétés énumérables d’un objet. Pour renvoyer un tableau des clés de iterObject , passez le mot-clé this comme argument à Object.keys .

Par exemple:

let objProperties = Object.keys(this)

L’accès à ce tableau vous permettra de définir le comportement d’itération de l’objet.

Ensuite, vous devez suivre les itérations de l’objet. Vous pouvez y parvenir en utilisant des variables de compteur.

Par exemple:

let propertyIndex = 0;
let childIndex = 0;

Vous utiliserez la première variable de compteur pour suivre les propriétés de l’objet et la seconde pour suivre les enfants de la propriété.

Ensuite, vous devrez implémenter et renvoyer la méthode suivante .

Ainsi:

return {
  next() {
    // Subsequent code blocks go here...
  }
}

Dans la méthode suivante , vous devrez gérer un cas limite qui se produit lorsque l’objet entier a été itéré. Pour gérer le cas limite, vous devez renvoyer un objet avec la valeur définie sur undefined et done définie sur true .

Voici comment gérer le cas marginal :

if (propertyIndex > objProperties.length - 1) {
  return {
    value: undefined,
    done: true,
  };
}

Ensuite, vous devrez accéder aux propriétés de l’objet et à leurs éléments enfants à l’aide des variables de compteur que vous avez déclarées précédemment.

Ainsi:

// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];

const property = properties[childIndex];

Ensuite, vous devez implémenter une logique pour incrémenter les variables de compteur. La logique doit réinitialiser le childIndex lorsqu’il n’existe plus d’éléments dans le tableau d’une propriété et passer à la propriété suivante dans l’objet. De plus, il doit incrémenter childIndex , s’il y a encore des éléments dans le tableau de la propriété actuelle.

Par exemple:

// Index incrementing logic
if (childIndex >= properties.length - 1) {
  // if there are no more elements in the child array
  // reset child index
  childIndex = 0;

  // Move to the next property
  propertyIndex++;
} else {
  // Move to the next element in the child array
  childIndex++
}

Enfin, renvoyez un objet avec la propriété done définie sur false et la propriété value définie sur l’élément enfant actuel dans l’itération.

Par exemple:

return {
  done: false,
  value: property,
};

Votre fonction Symbol.iterator terminée doit être similaire au bloc de code ci-dessous :

iterObject[Symbol.iterator] = function () {
  const objProperties = Object.keys(this);
  let propertyIndex = 0;
  let childIndex = 0;

  return {
    next: () => {
      //Handling edge case
      if (propertyIndex > objProperties.length - 1) {
        return {
          value: undefined,
          done: true,
        };
      }

      // Accessing parent and child properties
      const properties = this[objProperties[propertyIndex]];

      const property = properties[childIndex];

      // Index incrementing logic
      if (childIndex >= properties.length - 1) {
        // if there are no more elements in the child array
        // reset child index
        childIndex = 0;

        // Move to the next property
        propertyIndex++;
      } else {
        // Move to the next element in the child array
        childIndex++
      }

      return {
        done: false,
        value: property,
      };
    },
  };
};

L’exécution d’une boucle for…of sur iterObject après cette implémentation ne générera pas d’erreur car elle implémente une méthode Symbol.iterator .

L’implémentation manuelle des itérateurs, comme nous l’avons fait ci-dessus, n’est pas recommandée car elle est très sujette aux erreurs et la logique peut être difficile à gérer.

Générateurs JavaScript

Un générateur JavaScript est une fonction que vous pouvez mettre en pause et reprendre son exécution à tout moment. Ce comportement lui permet de produire une séquence de valeurs dans le temps.

Une fonction générateur, qui est une fonction qui renvoie un générateur, offre une alternative à la création d’itérateurs.

Vous pouvez créer une fonction de générateur de la même manière que vous créeriez une déclaration de fonction en JavaScript. La seule différence est que vous devez ajouter un astérisque ( * ) au mot-clé de la fonction.

Par exemple:

function* example () {
  return "Generator"
}

Lorsque vous appelez une fonction normale en JavaScript, elle renvoie la valeur spécifiée par son mot-clé return ou undefined sinon. Mais une fonction génératrice ne renvoie aucune valeur immédiatement. Elle renvoie un objet Generator, que vous pouvez affecter à une variable.

Pour accéder à la valeur actuelle de l’itérateur, appelez la méthode next sur l’objet Generator.

Par exemple:

const gen = example();

console.log(gen.next()); // { value: 'Generator', done: true }

Dans l’exemple ci-dessus, la propriété value provenait d’un mot-clé return , mettant ainsi fin au générateur. Ce comportement est généralement indésirable avec les fonctions de générateur, car ce qui les distingue des fonctions normales est la possibilité de suspendre et de redémarrer l’exécution.

Le rendement

Le mot-clé yield fournit un moyen de parcourir les valeurs dans les générateurs en interrompant l’exécution d’une fonction de générateur et en renvoyant la valeur qui la suit.

Par exemple:

function* example() {
  yield "Model S"
  yield "Model X"
  yield "Cyber Truck"

  return "Tesla"
}

const gen = example();

console.log(gen.next()); // { value: 'Model S', done: false }

Dans l’exemple ci-dessus, lorsque la méthode suivante est appelée sur le générateur d’exemple , elle s’interrompt chaque fois qu’elle rencontre le mot clé yield . La propriété done sera également définie sur false jusqu’à ce qu’elle rencontre un mot-clé de retour .

En appelant la méthode suivante plusieurs fois sur le générateur d’exemple pour le démontrer, vous aurez ce qui suit comme sortie.

console.log(gen.next()); // { value: 'Model X', done: false }
console.log(gen.next()); // { value: 'Cyber Truck', done: false }
console.log(gen.next()); // { value: 'Tesla', done: true }

console.log(gen.next()); // { value: undefined, done: true }

Vous pouvez également itérer sur un objet Generator en utilisant la boucle for…of .

Par exemple:

for (const iterator of gen) {
  console.log(iterator);
}

/*
Model S
Model X
Cyber Truck
*/

Utiliser des itérateurs et des générateurs

Bien que les itérateurs et les générateurs puissent sembler être des concepts abstraits, ils ne le sont pas. Ils peuvent être utiles lorsque vous travaillez avec des flux de données et des collections de données infinis. Vous pouvez également les utiliser pour créer des identifiants uniques. Les bibliothèques de gestion d’état telles que MobX-State-Tree (MST) les utilisent également sous le capot.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *