Comment créer facilement une liste d'éléments qu'il sera possible de parcourir à l'aide de l'instruction
for... of...
(à ne pas confondre avec for... in...
) ?
Cette liste doit pouvoir être totalement spécifique, et non seulement une liste prédéfinie dans JavaScript, comme notamment un tableau. En particulier, ses éléments doivent pouvoir n'être générés qu'au fil de l'itération.
La solution
La solution consiste à utiliser des fonctionnalités de JavaScript introduites dans la version 2015 du standard ECMAScript : un itérateur, créé par un générateur.
Le code JavaScript
En JavaScript, la solution se traduit par le code suivant :
function* iteratorGenerator (arrayToIterate) { var index; for (index = 0; index != arrayToIterate.length; index ++) yield arrayToIterate[index]; } var iteratorList, iteratorElement; iteratorList = iteratorGenerator (["Ceci", "est", "une", "itération"]); for (iteratorElement of iteratorList) alert (iteratorElement);
L'exemple
Cliquez ici pour accéder à une page de test minimaliste permettant de produire tester les différentes solutions proposées. Vous pourrez visualiser le code et le récupérer pour travailler avec.
La logique
Un itérateur est un objet comportant une méthode
next ()
retournant un objet comportant des propriétés value
et done
. Toutefois, l'itérateur n'est pas la liste des éléments, ce serait trop simple :
-
la liste des éléments est un objet qui doit implémenter l'interface
Iterable
, et à ce titre comporter un symboleSymbol.iterator
qui retourne un itérateur ; -
l'itérateur est un objet qui doit implémenter l'interface
Iterator
, et à ce titre comporter une méthodenext ()
qui retourne un objet correspondant à l'élément courant de la liste de données ; -
l'élément est un objet qui doit implémenter l'interface
IteratorResult
, et à ce titre comporter une propriétévalue
correspondant à la valeur et un propriétédone
dont le sens est évident (false
si la liste n'est pas épuisée,true
dans le cas contraire).
var iteratorList = { _index:-1, _data:["Ceci", "est", "une", "itération"] }; iteratorList[Symbol.iterator] = function () { return (function (iteratorListRef) { return ({ next:function () { if (!iteratorListRef._data.length) return ({value:null, done:true}); var done; if (iteratorListRef._index != (iteratorListRef._data.length - 1)) { done = false; iteratorListRef._index ++; } else done = true; return ({value:iteratorListRef._data[iteratorListRef._index], done:done}); } }); } (this)); } var iteratorElement; for (iteratorElement of iteratorList) alert (iteratorElement);
La liste utilisée ici est un simple wrapper d'un tableau de chaînes de caractères. Toutefois, il est clair qu'il pourrait s'agir d'un objet nettement plus complexe. En particulier, les éléments pourraient être générés au fil des itérations. Tout ce qui intéresse JavaScript exécutant l'instruction
for... of...
, c'est que l'itérateur recupéré via la propriété Symbol.iterator
retourne un élement tant qu'il doit retourner des éléments. Un itérateur apparaît ainsi comme un outil particulièrement souple.
Il est impossible de déclarer
Symbol.iterator
dans le corps de l'objet ; c'est pourquoi on procède par affectation.
var iteratorList = { _index:-1, _data:["Ceci", "est", "une", "itération"], Symbol.iterator = function () { // Ne fonctionne pas // ... } };
Comme on peut le constater,
iteratorList[Symbol.iterator]
retourne un itérateur créé par un constructeur anonyme. Toutefois, comment la méthode next ()
de cet itérateur peut-elle accéder au contenu de l'objet iteratorList
si ce dernier ne correspond pas à une variable globale ? La seule solution et d'utiliser une closure : il faut que next ()
maintienne une référence sur un variable transmise comme argument au constructeur de l'objet dont elle est une méthode. C'est pourquoi iteratorList[Symbol.iterator]
est un constructeur... :
iteratorList[Symbol.iterator] = function () { // ... }
...qui retourne le résultat d'un appel à un constructeur auquel une référence sur la liste des éléments est transmise par argument... :
return (function (iteratorListRef) { // ... } (this)
...qui retourne le résultat d'un appel à un constructeur, l'itérateur qui référence la liste des éléments ainsi maintenue :
return (function () { next:function () { // Utilisation de iteratorListRef } });
Il n'existe pas de solution pour réinitialiser l'itérateur. Une fois que
next ()
a retourné un élément dont la propriété done
est à false
, la méthode n'est plus appelée. Il faut donc permettre de créer l'itérateur plutôt que de le déclarer une fois, ce qui donne au code une autre forme :
function createiteratorList (arrayToIterate) { var iteratorList = { _index:-1, _data:arrayToIterate }; iteratorList[Symbol.iterator] = function () { return (function (iteratorListRef) { return ({ next:function () { if (!iteratorListRef._data.length) return ({value:null, done:true}); var done; if (iteratorListRef._index != (iteratorListRef._data.length - 1)) { done = false; iteratorListRef._index ++; } else done = true; return ({value:iteratorListRef._data[iteratorListRef._index], done:done}); } }); } (iteratorList)); } return (iteratorList); } var iteratorList, iteratorElement; iteratorList = createiteratorList (["Ceci", "est", "une", "itération"]); for (iteratorElement of iteratorList) alert (iteratorElement)
C'est pour simplifier cette mécanique que le concept de générateur a été introduit.
Un générateur est une fonction définie par
function*
. L'appel à cette fonction (qui n'est donc pas un constructeur, mais une factory, ne s'appelant par new
) retourne un itérateur dont le corps de la méthode next ()
correspond à celui de la fonction. L'état de cette fonction est donc conservé entre deux appels, ce qui permet de parcourir une liste d'éléments en retournant à chaque appel l'élément courant via l'instruction yield value
s'il reste possible d'itérer, et rien via l'instruction return
quand ce n'est plus le cas.
function* iteratorGenerator (arrayToIterate) { var index; for (index = 0; index != arrayToIterate.length; index ++) yield arrayToIterate[index]; } var iteratorList, iteratorElement; iteratorList = iteratorGenerator (["Ceci", "est", "une", "itération"]); for (iteratorElement of iteratorList) alert (iteratorElement);
Attention ! L'instruction
yield
suspend l'exécution de l'itérateur qui reprend immédiatement après cette instruction. Ce n'est donc pas une forme d'instruction return
:
function* iteratorGenerator (arrayToIterate) { var index; for (index = 0; index != arrayToIterate.length; index ++) { yield arrayToIterate[index]; alert ("ok"); // L'exécution reprend ici : "ok" est affiché à chaque appel à next () à partir du second appel à next () } }
Le recours à un générateur permet donc d'écrire une version simplifiée du premier code présenté (l'objet in-line) :
var iteratorList = { _index:-1, _data:["Ceci", "est", "une", "itération"] }; iteratorList[Symbol.iterator] = function* () { for (this._index = 0; this._index != this._data.length; this._index ++) yield this._data[this._index]; } var iteratorList; for (iteratorElement of iteratorList) alert (iteratorElement);
L'instruction
yield
retourne la valeur qui est transmise à next ()
, ce qui permet au code appelant l'itérateur de modifier le comportement de ce dernier. Typiquement, ce code transmettra une valeur pour réinitialiser l'itérateur :
var iteratorList = { _index:-1, _data:["Ceci", "est", "une", "itération"] }; iteratorList[Symbol.iterator] = function* () { var resetIterator; this._index = 0; while (this._index != this._data.length) { resetIterator = yield this._data[this._index]; if (resetIterator) this._index = 0; else this._index ++; } } var iterator; iterator = iteratorList[Symbol.iterator] (); alert (iterator.next (false).value); // Affiche "Ceci" alert (iterator.next (false).value); // Affiche "est" alert (iterator.next (true).value); // Affiche "Ceci" alert (iterator.next (false).value); // Affiche "est"