Cet article est une introduction approfondie aux itérables et aux itérateurs en JavaScript. Ma principale motivation pour écrire ceci était de me préparer à en apprendre davantage sur les générateurs. En fait, j'avais l'intention d'expérimenter plus tard en combinant des générateurs et des crochets React. Si vous êtes intéressé, suivez mon Twitter ou YouTube !
En fait, j'avais prévu de commencer par un article sur les générateurs, mais il est vite devenu évident qu'il est difficile d'en parler sans une bonne compréhension des itérables et des itérateurs. Nous allons nous concentrer sur eux maintenant. Je suppose que vous ne savez rien sur ce sujet, mais en même temps, nous allons approfondir considérablement. Alors si tu es quelque chose connaissez les itérateurs et les itérateurs, mais ne vous sentez pas à l'aise de les utiliser, cet article vous aidera.
introduction
Comme vous l'avez remarqué, nous discutons des itérables et des itérateurs. Ces concepts sont interdépendants, mais différents, alors lors de la lecture de l'article, faites attention à celui qui est discuté dans un cas particulier.
Commençons par les objets itérables. Ce que c'est? C'est quelque chose qui peut être répété, par exemple:
for (let element of iterable) {
// do something with an element
}
Veuillez noter que nous ne regardons ici que les boucles
for ... of
qui ont été introduites dans ES6. Et les boucles
for ... in
sont une construction plus ancienne à laquelle nous ne ferons pas du tout référence dans cet article.
Maintenant, vous pensez peut-être: "D'accord, cette variable itérable n'est qu'un tableau!" C'est vrai, les tableaux sont itérables. Mais maintenant, il existe d'autres structures en JavaScript natif que vous pouvez utiliser dans une boucle
for ... of
. Autrement dit, en plus des tableaux, il existe d'autres objets itérables.
Par exemple, nous pouvons itérer
Map
, introduit dans ES6:
const ourMap = new Map();
ourMap.set(1, 'a');
ourMap.set(2, 'b');
ourMap.set(3, 'c');
for (let element of ourMap) {
console.log(element);
}
Ce code affichera:
[1, 'a']
[2, 'b']
[3, 'c']
Autrement dit, la variable
element
à chaque étape d'itération stocke un tableau de deux éléments. Le premier est la clé, le second est la valeur.
Le fait que nous ayons pu utiliser une boucle
for ... of
pour itérer
Map
prouve qu'il
Map
est itérable. Encore une fois
for ... of
, seuls les objets itérables peuvent être utilisés dans les boucles . Autrement dit, si quelque chose fonctionne avec cette boucle, alors c'est un objet itérable.
C'est drôle que le constructeur
Map
accepte éventuellement les itérables de paires clé-valeur. Autrement dit, il s'agit d'une autre manière de construire la même chose
Map
:
const ourMap = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c'],
]);
Et comme il
Map
s'agit d'un itérable, nous pouvons en faire des copies très facilement:
const copyOfOurMap = new Map(ourMap);
Nous en avons maintenant deux différents
Map
, bien qu'ils stockent les mêmes clés avec les mêmes valeurs.
Nous avons donc vu deux exemples d'objets itérables - array et ES6
Map
. Mais nous ne savons pas encore comment ils ont pu être itérables. La réponse est simple: il y a des itérateurs qui leur sont associés . Attention: les itérateurs ne sont pas itérables .
Comment un itérateur est-il associé à un objet itérable? Un objet simplement itérable doit contenir une fonction dans sa propriété
Symbol.iterator
. Lorsqu'elle est appelée, la fonction doit renvoyer un itérateur pour cet objet.
Par exemple, vous pouvez récupérer un itérateur de tableau:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
console.log(iterator);
Ce code sort sur la console
Object [Array Iterator] {}
. Nous savons maintenant que le tableau a un itérateur associé, qui est une sorte d'objet.
Qu'est-ce qu'un itérateur?
C'est simple. Un itérateur est un objet contenant une méthode
next
. Lorsque cette méthode est appelée, elle doit renvoyer:
- la valeur suivante dans une séquence de valeurs;
- des informations indiquant si l'itérateur a fini de générer des valeurs.
Testons cela en appelant une méthode
next
sur notre itérateur de tableau:
const result = iterator.next();
console.log(result);
Nous verrons l'objet dans la console
{ value: 1, done: false }
. Le premier élément du tableau que nous avons créé est 1, et ici il est apparu comme une valeur. Nous avons également reçu des informations indiquant que l'itérateur n'a pas encore terminé, c'est-à-dire que nous pouvons toujours appeler la fonction
next
et obtenir des valeurs. Essayons! Appelons-le
next
deux fois de plus:
console.log(iterator.next());
console.log(iterator.next());
Reçu un par un
{ value: 2, done: false }
et
{ value: 3, done: false }
.
Il n'y a que trois éléments dans notre tableau. Que se passe-t-il si vous l'appelez à nouveau
next
?
console.log(iterator.next());
Cette fois, nous verrons
{ value: undefined, done: true }
. Cela indique que l'itérateur est terminé. Il ne sert à rien de rappeler
next
. Si nous faisons cela, nous recevrons encore et encore un objet
{ value: undefined, done: true }
.
done: true
signifie arrêter d'itérer.
Vous pouvez maintenant comprendre ce qu'il fait
for ... of
sous le capot:
- la première méthode
[Symbol.iterator]()
est appelée pour obtenir l'itérateur; - la méthode
next
est appelée cycliquement sur l'itérateur jusqu'à ce que nous l'obtenionsdone: true
; - après chaque appel
next
, la propriété est utilisée dans le corps de la bouclevalue
.
Écrivons tout cela en code:
const iterator = ourArray[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const element = result.value;
// do some something with element
result = iterator.next();
}
Ce code est équivalent à ceci:
for (let element of ourArray) {
// do something with element
}
Vous pouvez le vérifier, par exemple, en insérant à la
console.log(element)
place d'un commentaire
// do something with element
.
Créez votre propre itérateur
Nous savons maintenant ce que sont les itérables et les itérateurs. La question se pose: "Puis-je écrire mes propres instances?"
Certainement!
Il n'y a rien de mystérieux dans les itérateurs. Ce ne sont que des objets avec une méthode
next
qui se comportent d'une manière spéciale. Nous avons déjà déterminé quelles valeurs natives dans JS sont itérables. Aucun objet n'a été mentionné parmi eux. En effet, ils ne sont pas itérés nativement. Considérez un objet comme celui-ci:
const ourObject = {
1: 'a',
2: 'b',
3: 'c'
};
Si nous itérons avec
for (let element of ourObject)
, nous obtenons une erreur
object is not iterable
.
Écrivons nos propres itérateurs en rendant un tel objet itérable!
Pour ce faire, vous devez patcher le prototype
Object
avec votre propre méthode
[Symbol.iterator]()
. Puisque patcher le prototype est une mauvaise pratique, créons notre propre classe en étendant
Object
:
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
}
Le constructeur de notre classe prend un objet ordinaire et copie ses propriétés dans un objet itérable (bien qu'il ne soit pas encore itérable!).
Créons un objet itérable:
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
})
Pour rendre une classe
IterableObject
vraiment itérable, nous avons besoin d'une méthode
[Symbol.iterator]()
. Ajoutons-le.
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
[Symbol.iterator]() {
}
}
Vous pouvez maintenant écrire un véritable itérateur!
On sait déjà que ce doit être un objet avec une méthode
next
. Commençons par ceci.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
return {
next() {}
}
}
}
Après chaque appel,
next
vous devez renvoyer un objet de vue
{ value, done }
. Faisons-le avec des valeurs fictives.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
Étant donné un objet itérable comme celui-ci:
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
})
nous allons générer des paires clé-valeur, comme le fait l'itération ES6
Map
:
['1', 'a']
['2', 'b']
['3', 'c']
Dans notre itérateur,
property
nous allons stocker un tableau dans la valeur
[key, valueForThatKey]
. Veuillez noter qu'il s'agit de notre propre solution par rapport aux étapes précédentes. Si nous voulions écrire un itérateur qui ne renvoie que des clés ou uniquement des valeurs de propriété, nous pourrions le faire sans aucun problème. Nous avons juste décidé de renvoyer des paires clé-valeur maintenant.
Nous avons besoin d'un tableau du type
[key, valueForThatKey]
. Le moyen le plus simple de l'obtenir est d'utiliser la méthode
Object.entries
. Nous pouvons l'utiliser juste avant de créer l'objet itérateur dans la méthode
[Symbol.iterator]()
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// we made an addition here
const entries = Object.entries(this);
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
L'itérateur retourné dans la méthode accédera à la variable grâce à la fermeture JavaScript
entries
.
Nous avons également besoin d'une variable d'état. Il nous dira quelle paire clé-valeur doit être retournée lors du prochain appel
next
. Ajoutons-le:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
// we made an addition here
let index = 0;
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
Notez que nous avons déclaré la variable
index
c
let
car nous savons que nous prévoyons de mettre à jour sa valeur après chaque appel
next
.
Nous sommes maintenant prêts à renvoyer la valeur réelle dans la méthode
next
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
return {
// we made a change here
value: entries[index],
done: false
}
}
}
}
}
C'était facile. Nous n'utilisons que des variables
entries
et
index
pour accéder à la bonne paire clé-valeur à partir du tableau
entries
.
Nous devons maintenant nous occuper de la propriété
done
, car elle le sera toujours
false
. Vous pouvez créer une autre variable en plus de
entries
et
index
, et la mettre à jour après chaque appel
next
. Mais il existe un moyen encore plus simple. Vérifions si
index
le tableau est hors limites
entries
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
return {
value: entries[index],
// we made a change here
done: index >= entries.length
}
}
}
}
}
Notre itérateur se termine lorsque la variable
index
est égale ou supérieure à la longueur
entries
. Par exemple, si y a une
entries
longueur de 3, alors il contient des valeurs aux indices 0, 1 et 2. Et lorsque la variable
index
est égale ou supérieure à 3, cela signifie qu'il ne reste plus de valeurs. Avaient fini.
Ce code fonctionne presque . Il ne reste qu'une chose à ajouter.
La variable
index
commence à 0, mais ... nous ne la mettons pas à jour! Ce n'est pas aussi simple. Nous devons mettre à jour la variable après notre retour
{ value, done }
. Mais quand nous l'avons rendu, la méthode
next
s'arrête immédiatement même s'il y a du code après l'expression
return
. Mais nous pouvons créer un objet
{ value, done }
, le stocker dans une variable, le mettre à jour
index
et seulement ensuite renvoyer l'objet:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
const result = {
value: entries[index],
done: index >= entries.length
};
index++;
return result;
}
}
}
}
Après nos modifications, la classe
IterableObject
ressemble à ceci:
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
const result = {
value: entries[index],
done: index >= entries.length
};
index++;
return result;
}
}
}
}
Le code fonctionne très bien, mais il est devenu assez déroutant. C'est parce qu'il montre une manière plus intelligente mais moins évidente de mettre à jour
index
après la création d'objet
result
. Nous pouvons simplement initialiser
index
à -1! Et bien qu'il soit mis à jour avant le retour de l'objet
next
, tout fonctionnera correctement, car la première mise à jour remplacera -1 par 0.
Alors faisons-le:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = -1;
return {
next() {
index++;
return {
value: entries[index],
done: index >= entries.length
}
}
}
}
}
Comme vous pouvez le voir, nous n'avons plus besoin de jongler avec l'ordre de création
result
et de mise à jour des objets
index
. Lors du deuxième appel, il
index
sera mis à jour à 1, et nous retournerons un résultat différent, etc. Tout fonctionne comme nous le voulions et le code semble beaucoup plus simple.
Mais comment vérifier l'exactitude du travail? Vous pouvez exécuter manuellement une méthode
[Symbol.iterator]()
pour instancier un itérateur, puis vérifier directement les résultats des appels
next
. Mais vous pouvez faire beaucoup plus facilement! Il a été dit plus haut que tout objet itérable peut être inséré dans une boucle
for ... of
. Faisons juste cela, en enregistrant les valeurs renvoyées par notre objet itérable en cours de route:
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
});
for (let element of iterableObject) {
console.log(element);
}
Travaux! Voici ce qui s'affiche dans la console:
[ '1', 'a' ]
[ '2', 'b' ]
[ '3', 'c' ]
Cool! Nous avons commencé avec un objet qui ne pouvait pas être utilisé dans les boucles
for ... of
, car ils ne contiennent pas nativement des itérateurs intégrés. Mais nous avons créé le nôtre
IterableObject
, qui a un itérateur auto-écrit associé.
J'espère que vous pouvez maintenant voir le potentiel des itérables et des itérateurs. C'est un mécanisme qui vous permet de créer vos propres structures de données pour travailler avec des fonctions JS comme des boucles
for ... of
, et elles fonctionnent comme des structures natives! C'est une fonctionnalité très utile qui peut grandement simplifier votre code dans certaines situations, en particulier si vous prévoyez d'itérer fréquemment vos structures de données.
De plus, nous pouvons personnaliser ce que ces itérations doivent renvoyer exactement. Notre itérateur renvoie maintenant des paires clé-valeur. Et si nous ne voulons que des valeurs? Facile, il suffit de réécrire l'itérateur:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// changed `entries` to `values`
const values = Object.values(this);
let index = -1;
return {
next() {
index++;
return {
// changed `entries` to `values`
value: values[index],
// changed `entries` to `values`
done: index >= values.length
}
}
}
}
}
Et c'est tout! Si nous démarrons maintenant la boucle
for ... of
, nous verrons dans la console:
a b c
Nous n'avons renvoyé que les valeurs des objets. Tout cela prouve la flexibilité des itérateurs auto-écrits. Vous pouvez leur faire rendre ce que vous voulez.
Les itérateurs comme ... objets itérables
Il est très courant que les gens confondent les itérateurs et les itérables. C'est une erreur et j'ai essayé de bien séparer les deux. Je soupçonne que je connais la raison pour laquelle les gens les confondent si souvent.
Il s'avère que les itérateurs ... sont parfois itérables!
Qu'est-ce que ça veut dire? Souvenez-vous qu'un itérable est l'objet auquel un itérateur est associé. Chaque itérateur JavaScript natif a une méthode
[Symbol.iterator]()
qui renvoie un autre itérateur! Cela fait du premier itérateur un objet itérable.
Vous pouvez vérifier cela si vous prenez un itérateur renvoyé par un tableau et que vous l'appelez
[Symbol.iterator]()
:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
const secondIterator = iterator[Symbol.iterator]();
console.log(secondIterator);
Après avoir exécuté ce code, vous verrez
Object [Array Iterator] {}
. Autrement dit, un itérateur contient non seulement un autre itérateur qui lui est associé, mais aussi un tableau.
Si vous comparez les deux itérateurs avec,
===,
il s'avère qu'ils sont exactement les mêmes:
const iterator = ourArray[Symbol.iterator]();
const secondIterator = iterator[Symbol.iterator]();
// logs `true`
console.log(iterator === secondIterator);
Au début, vous pouvez trouver étrange le comportement d'un itérateur qui est son propre itérateur. Mais c'est une fonctionnalité très utile. Vous ne pouvez pas coller un itérateur nu dans une boucle
for ... of
, il n'accepte qu'un objet itérable - un objet avec une méthode
[Symbol.iterator]()
.
Cependant, la situation où un itérateur est son propre itérateur (et donc un objet itérable) cache le problème. Puisque les itérateurs JS natifs contiennent des méthodes
[Symbol.iterator]()
, vous pouvez les passer directement dans des boucles sans arrière-pensée
for ... of
.
En conséquence, cet extrait:
const ourArray = [1, 2, 3];
for (let element of ourArray) {
console.log(element);
}
et celui-là:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
travailler de manière transparente et faire la même chose. Mais pourquoi quelqu'un utiliserait-il des itérateurs comme celui-ci dans des boucles directement
for ... of
? Parfois, c'est juste inévitable.
Tout d'abord, vous devrez peut-être créer un itérateur sans appartenir à un itérable. Nous allons regarder cet exemple ci-dessous, et ce n'est pas rare. Parfois, nous n'avons tout simplement pas besoin de l'itérable lui-même.
Et ce serait très gênant si le fait d'avoir un itérateur nu signifiait que vous ne pouvez pas l'utiliser
for ... of
. Bien sûr, vous pouvez le faire manuellement en utilisant une méthode
next
et, par exemple, une boucle
while
, mais nous avons vu que pour cela, vous devez écrire beaucoup de code, de plus, répétitif.
La solution est simple: si vous voulez éviter le code standard et utiliser un itérateur dans une boucle
for ... of
, vous devez faire de l'itérateur un objet itérable.
D'autre part, nous obtenons également assez souvent des itérateurs à partir de méthodes autres que
[Symbol.iterator]()
. Par exemple, ES6
Map
contient des méthodes
entries
,
values
et
keys
. Ils renvoient tous des itérateurs.
Si les itérateurs JS natifs n'étaient pas également des objets itérables, vous ne pouviez pas utiliser ces méthodes directement dans des boucles
for ... of
, comme ceci:
for (let element of map.entries()) {
console.log(element);
}
for (let element of map.values()) {
console.log(element);
}
for (let element of map.keys()) {
console.log(element);
}
Ce code fonctionne car les itérateurs renvoyés par les méthodes sont également des objets itérables. Sinon, vous devrez, par exemple, envelopper le résultat de l'appel
map.entries()
dans un objet itérable stupide. Heureusement, nous n'avons pas besoin de faire cela.
Il est recommandé de créer vos propres objets itérables. Surtout s'ils sont renvoyés par des méthodes autres que
[Symbol.iterator]()
. Faire d'un itérateur un objet itérable est très simple. Laissez-moi vous montrer avec un exemple d'itérateur
IterableObject
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// same as before
return {
next() {
// same as before
},
[Symbol.iterator]() {
return this;
}
}
}
}
Nous avons créé une méthode
[Symbol.iterator]()
sous la méthode
next
. Fait de cet itérateur son propre itérateur juste en retournant
this
, ce qui signifie qu'il se retourne lui-même. Ci-dessus, nous avons déjà vu comment se comporte un itérateur de tableau. Cela suffit pour que notre itérateur fonctionne en boucle,
for ... of
même directement.
État de l'itérateur
Il devrait maintenant être évident que chaque itérateur a un état qui lui est associé. Par exemple, dans un itérateur,
IterableObject
nous avons stocké un état - une variable
index
- en tant que fermeture. Et nous l'avons mis à jour après chaque étape d'itération.
Que se passe-t-il une fois le processus d'itération terminé? L'itérateur devient inutile et vous pouvez (devriez!) Le supprimer. Vous pouvez voir que cela se produit même par l'exemple des objets JS natifs. Prenons un itérateur de tableau et essayons de l'exécuter deux fois dans une boucle
for ... of
.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
for (let element of iterator) {
console.log(element);
}
Vous pouvez vous attendre à ce que la console affiche les nombres deux fois
1
,
2
et
3
. Mais le résultat sera comme ceci:
1
2
3
Pourquoi?
Appelons manuellement
next
après la fin de la boucle:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
console.log(iterator.next());
Le dernier journal est sorti sur la console
{ value: undefined, done: true }
.
C'est ça. Une fois la boucle terminée, l'itérateur passe à l'état «terminé». Maintenant, il retournera toujours un objet
{ value: undefined, done: true }
.
Existe-t-il un moyen de "réinitialiser" l'état de l'itérateur afin qu'il puisse être utilisé une deuxième fois
for ... of
? Dans certains cas, c'est possible, mais cela n'a aucun sens. C'est donc
[Symbol.iterator]
une méthode, pas seulement une propriété. Vous pouvez appeler à nouveau la méthode et obtenir un autre itérateur:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
const secondIterator = ourArray[Symbol.iterator]();
for (let element of secondIterator) {
console.log(element);
}
Tout fonctionne maintenant comme prévu. Voyons pourquoi plusieurs boucles avant dans le tableau fonctionnent:
const ourArray = [1, 2, 3];
for (let element of ourArray) {
console.log(element);
}
for (let element of ourArray) {
console.log(element);
}
Toutes les boucles
for ... of
utilisent des itérateurs différents ! Une fois l'itérateur et la boucle terminés, cet itérateur n'est plus utilisé.
Itérateurs et tableaux
Puisque nous utilisons des itérateurs (bien qu'indirectement) dans les boucles
for ... of
, ils peuvent ressembler de manière trompeuse à des tableaux. Mais il existe deux différences importantes. Iterator et Array utilisent les concepts de valeurs gourmandes et paresseuses. Lorsque vous créez un tableau, à un moment donné, il a une certaine longueur et ses valeurs sont déjà initialisées. Bien sûr, vous pouvez créer un tableau sans aucune valeur, mais ce n'est pas le cas. Mon point est qu'il n'est pas possible de créer un tableau qui initialise ses valeurs uniquement après y avoir accédé en écrivant
array[someIndex]
. Il peut être possible de contourner ce problème avec un proxy ou une autre astuce, mais par défaut, les tableaux JavaScript ne se comportent pas de cette façon.
Et quand ils disent qu'un tableau a une longueur, ils signifient que cette longueur est finie. Il n'y a pas de tableaux infinis en JavaScript.
Ces deux qualités indiquent que les tableaux sont gourmands .
Et les itérateurs sont paresseux .
Pour le montrer, nous allons créer deux de nos itérateurs: le premier sera infini, contrairement aux tableaux finis, et le second n'initialisera ses valeurs que lorsqu'elles seront demandées par l'utilisateur de l'itérateur.
Commençons par un itérateur infini. Cela semble intimidant, mais c'est très simple à créer: l'itérateur commence à 0 et renvoie le numéro suivant de la séquence à chaque étape. Pour toujours.
const counterIterator = {
integer: -1,
next() {
this.integer++;
return { value: this.integer, done: false };
},
[Symbol.iterator]() {
return this;
}
}
Et c'est tout! Nous avons commencé avec une propriété de
integer
-1. Chaque fois que
next
nous l' appelons, nous l'incrémentons de 1 et le renvoyons sous forme d'objet
value
. Notez que nous avons utilisé à nouveau l'astuce ci-dessus: nous avons commencé à -1 pour retourner la première fois 0.
Jetez également un œil à la propriété
done
. Ce sera toujours faux. Cet itérateur ne se termine pas!
De plus, nous avons fait de l'itérateur un itérateur en lui donnant une implémentation simple
[Symbol.iterator]()
.
Une dernière chose: c'est le cas que j'ai mentionné ci-dessus - nous avons créé un itérateur, mais il n'a pas besoin d'un parent itérable pour fonctionner.
Essayons maintenant cet itérateur en boucle
for ... of
. Vous devez juste vous rappeler d'arrêter la boucle à un moment donné, sinon le code sera exécuté pour toujours.
for (let element of counterIterator) {
if (element > 5) {
break;
}
console.log(element);
}
Après le lancement, nous verrons dans la console:
0
1
2
3
4
5
Nous avons en fait créé un itérateur infini qui renvoie autant de nombres que vous le souhaitez. Et c'était très facile de le faire!
Maintenant, écrivons un itérateur qui ne crée pas de valeurs tant qu'elles ne sont pas demandées.
Eh bien ... nous l'avons déjà fait!
Avez-vous remarqué qu'un
counterIterator
seul numéro de propriété est stocké à un moment donné
integer
? Il s'agit du dernier numéro renvoyé lors de l'appel
next
. Et c'est la même paresse. Un itérateur peut potentiellement renvoyer n'importe quel nombre (plus précisément, un entier positif). Mais il ne les crée que lorsqu'ils sont nécessaires: lorsque la méthode est appelée
next
.
Cela peut sembler assez astucieux. Après tout, les nombres sont créés rapidement et ne prennent pas beaucoup d'espace mémoire. Mais si vous travaillez avec des objets très volumineux qui prennent beaucoup de mémoire, le remplacement des tableaux par des itérateurs peut parfois être très utile, en accélérant le programme et en économisant de la mémoire.
Plus l'objet est grand (ou plus sa création prend du temps), plus le bénéfice est grand.
Autres façons d'utiliser les itérateurs
Jusqu'à présent, nous n'avons consommé des itérateurs que dans une boucle
for ... of
ou manuellement en utilisant le
next
. Mais ce ne sont pas les seuls moyens.
Nous avons déjà vu que le constructeur
Map
prend les itérables comme argument. Vous pouvez également
Array.from
facilement convertir un itérable en tableau à l'aide de la méthode . Mais fais attention! Comme je l'ai dit, la paresse de l'itérateur peut parfois être un gros avantage. La conversion en un tableau enlève la paresse. Toutes les valeurs renvoyées par l'itérateur sont initialisées immédiatement, puis placées dans un tableau. Cela signifie que si nous essayons de convertir l'infini
counterIterator
en tableau, cela conduira au désastre.
Array.from
s'exécutera pour toujours sans renvoyer de résultat. Donc, avant de convertir un itérable / itérateur en tableau, vous devez vous assurer que l'opération est sûre.
Fait intéressant, les itérables fonctionnent également bien avec l'opérateur de propagation
(...
.) N'oubliez pas que cela fonctionne de la même
Array.from
manière lorsque toutes les valeurs de l'itérateur sont générées en même temps. Par exemple, vous pouvez créer votre propre version à l'aide de l'opérateur de diffusion
Array.from
. Appliquez simplement l'opérateur à l'itérable, puis mettez les valeurs dans un tableau:
const arrayFromIterator = [...iterable];
Vous pouvez également obtenir toutes les valeurs de l'objet itérable et les appliquer à la fonction:
someFunction(...iterable);
Conclusion
J'espère que vous comprenez maintenant le titre de l'article Objets itérables et itérateurs. Nous avons appris ce qu'ils sont, en quoi ils diffèrent, comment les utiliser et comment créer nos propres instances. Nous sommes maintenant complètement prêts à travailler avec des générateurs. Si vous êtes familier avec les itérateurs, passer au sujet suivant ne devrait pas être trop difficile.