Itérateur et générateur en JavaScript

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 symbole Symbol.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éthode next () 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"
Itérateur et générateur en JavaScript