Itérables et itérateurs: un guide détaillé de JavaScript



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'obtenions done: true



    ;
  • après chaque appel next



    , la propriété est utilisée dans le corps de la boucle value



    .


É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.



All Articles