ES6 en détails est une série d’articles décrivant les nouvelles fonctionnalités ajoutées au langage de programmation JavaScript avec la sixième édition du standard ECMAScript (ES6 en abrégé).

Comment itérer sur les éléments d’un tableau ? Lorsque JavaScript est apparu, il y a vingt ans de ça, on aurait fait ainsi :

for (var index = 0; index < monTableau.length; index++) {
  console.log(monTableau[index]);
}

Depuis ES5, on peut utiliser la méthode native forEach :

monTableau.forEach(function (value) {
  console.log(value);
});

C’est un petit peu plus court, mais il y a un léger inconvénient : il n’est pas possible de sortir de cette boucle avec une instruction break ou de faire retourner la fonction englobante avec une instruction return.

Ce serait effectivement agréable s’il y avait une syntaxe de boucle for itérant directement sur les éléments du tableau.

Quid d’une boucle for-in ?

for (var index in monTableau) {    
  // ne faites pas ceci
  console.log(monTableau[index]);
}

C’est une mauvaise idée pour plusieurs raisons :

  1. Les valeurs assignées pour l’index dans ce code sont les chaînes de caractères "0", "1", "2", et ainsi de suite : ce ne sont pas réellement des nombres. Étant donné qu’on ne souhaite sûrement pas manipuler des chaînes de façon arithmétique ("2" + 1 == "21"), c’est, a minima, peu pratique ;
  2. Le corps de la boucle ne va pas seulement s’exécuter pour chaque élément du tableau, mais également pour toutes les propriétés non-natives que l’on pourrait avoir ajoutées. Par exemple, si votre tableau possède une propriété énumérable monTableau.nom, alors cette boucle va s’exécuter une fois de plus, avec index == "nom". Même les propriétés présentes sur la chaîne de prototypes du tableau peuvent être visitées ;
  3. Le plus étonnant dans tout ça est que, dans certaines circonstances, ce code va boucler sur les éléments du tableau dans un ordre arbitraire.

Pour faire court, for-in a été pensé pour travailler avec de bons vieux objets (type Object) dont les clés sont des chaînes de caractères. Pour les tableaux (type Array), ce n’est pas génial.

La puissante boucle for-of

Vous souvenez-vous de la semaine dernière, lorsque j’ai promis qu’ES6 ne casserait pas le code JavaScript que vous avez déjà écrit ? En fait, il existe des millions de sites Web qui dépendent du comportement de for-in — oui, même de son comportement sur les tableaux. Il n’a donc jamais été question de « réparer » for-in afin qu’elle soit plus utile pour manipuler avec des tableaux. Le seul moyen pour ES6 d’améliorer les choses était d’ajouter une nouvelle forme de syntaxe de boucle.

La voilà :

for (var valeur of monTableau) {
  console.log(valeur);
}

Hmm. Après toute cette publicité, ça ne semble pas si impressionnant, n’est-ce pas ? Pourtant, nous allons voir que for-of a plus d’un tour dans son sac. Pour l’instant, notez simplement que :

  1. C’est la syntaxe la plus concise et la plus directe pour boucler sur les éléments d’un tableau ;
  2. Celle-ci permet d’éviter les pièges de for-in ;
  3. Contrairement à forEach(), elle est compatible avec les instructions break, continue et return.

La boucle for-in est faite pour boucler sur les propriétés d’un objet.

La boucle for-of est faite pour boucler sur des données — comme les valeurs d’un tableau.

Mais ce n’est pas tout.

for-of fonctionne également avec les autres collections

for-of n’est pas restreint aux tableaux. Cela fonctionne également avec la plupart des objets qui agissent comme des tableaux, telles que les NodeList du DOM. Cela fonctionne également sur les chaînes de caractères, en considérant que la chaîne est une séquence de caractères Unicode :

for (var chr of "♂ ♀"){
  alert(chr);
}

Cela fonctionne également avec les objets Map et Set.

Oh, désolé si vous n’avez jamais entendu parler des objets Map et Set, ceux-ci sont des nouveautés apportées par ES6. Nous en parlerons plus tard dans un article dédié. Si vous avez déjà utilisé des Maps (dictionnaires) et Sets (ensembles) dans d’autres langages, vous ne serez pas très surpris.

Par exemple, un objet Set peut être utilisé pour éliminer des doublons :

// créons un ensemble à partir d'un tableau de mots
var motsUniques = new Set(mots);

Une fois que l’on a un ensemble (Set), que se passe-t-il si on veut itérer sur son contenu ? Facile :

for (var mot of motsUniques) {
  console.log(mot);
}

Une Map est légèrement différente : les données que celle-ci contient sont des paires de clés-valeurs, il faudra donc utiliser l’affectation « destructurée » (destructuring) pour séparer la clé et la valeur en deux variables distinctes :

for (var [clé, valeur] of annuaireMap) {
  console.log("Le numéro de téléphone de "+clé +" est " + valeur);
}

La décomposition est une autre fonctionnalité, également apparue avec ES6 qui fera un sujet idéal pour un prochain article que j’écrirai.

Maintenant, vous avez une idée du tableau : JS possède déjà un certain nombre de structures de données pour des collections, et il y en a encore plus à venir. for-of a été pensé pour être l’instruction de boucle ultime à utiliser avec toutes ces structures.

for-of ne fonctionne pas avec des objets tout simples, mais si vous voulez itérer sur les propriétés d’un objet, vous pouvez soit utiliser for-in (c’est sa raison d’être), soit la fonction native Object.keys() :

// affiche toutes les propriétés propres énumérables 
// d'un objet dans la console
for (var clé of Object.keys(monObjet)) {
  console.log(clé + ": " + monObjet[clé]);
}

Sous le capot

« Les bons artistes copient, les très bons artistes volent » — Pablo Picasso

Un thème récurrent d’ES6 est le fait que les nouvelles fonctionnalités ajoutées au langage ne viennent pas de nulle part. La plupart ont été essayées et se sont avérées utiles dans d’autres langages.

La boucle for-of, par exemple, rappelle des instructions de boucle similaires en C++, Java, C#, et Python. Comme celles-ci, elle fonctionne avec des structures de données différentes, fournies par le langage et sa bibliothèque standard. Mais c’est également un ajout au langage.

De même qu’avec les instructions for ou forEach dans ces autres langages, for-of fonctionne entièrement à l’aide d’appels de fonctions. Ce que les tableaux (objets Array), dictionnaires (objets Map), ensembles (objets Set) et les autres objets ont en commun est le fait qu’ils ont chacun une méthode iterator (NdT : pour « itérateur »).

Il existe une autre catégorie d’objets qui peuvent aussi avoir une méthode iterator : n’importe quel objet.

De la même manière que lorsqu’on ajoute une méthode monObjet.toString() à n’importe quel objet, JS sait comment convertir cet objet en une chaîne de caractères, on peut ajouter la méthode monObjetSymbol.iterator() à n’importe quel objet, et, soudainement, JS sait comment itérer sur cet objet.

Par exemple, imaginons que vous utilisiez jQuery, et que, bien que vous soyez très fan de .each(), vous aimeriez que les objets jQuery fonctionnent également avec for-of. Voici la manière de procéder :

// Comme les objets jQuery se comportent comme des tableaux,
// donnons-leur la même méthode iterator que celle de l'objet Array
jQuery.prototype[Symbol.iterator] =
Array.prototype[Symbol.iterator];

OK, je sais ce que vous êtes en train de vous dire. Cette syntaxe Symbol.iterator est bizarre. Que se passe-t-il ici ? C’est en lien avec le nom de la méthode. Le comité de standardisation aurait pu appeler cette méthode .iterator(), or dans ce cas, votre code aurait déjà pu comporter des objets ayant une méthode .iterator(), et cela aurait pu être une source de confusion. C’est pourquoi le standard préfère utiliser un symbole plutôt qu’une chaine de caractères comme nom de la méthode.

Les symboles sont apparus avec ES6, et nous vous expliquerons tout sur eux — vous l’avez deviné — dans un prochain article de blog. Pour l’instant, tout ce que vous avez besoin de savoir, c’est que le standard peut définir un symbole entièrement nouveau, comme Symbol.iterator, et qu’il est garanti que cela ne créera pas de conflit avec du code existant. Le compromis que cela implique est une syntaxe un peu étrange. Mais c’est un petit prix à payer pour cette nouvelle fonctionnalité versatile et la garantie d’une excellente rétro-compatibilité.

Un objet qui possède une méthode Symbol.iterator() est appelé un itérable. Dans les semaines qui viennent, nous verrons que le concept d’objets itérables est utilisé à travers tout le langage, pas seulement avec for-of mais également avec les constructeurs des objets Map et Set, avec les affectations déstructurées et avec le nouvel opérateur « spread » (NdT : ou « opérateur de décomposition »).

Les objets itérateurs

Il est probable que vous n’ayez jamais à implémenter un itérateur de A à Z. On verra pourquoi la semaine prochaine. Cependant, pour être tout à fait complet, regardons de près à quoi ressemble un objet itérable (si vous sautez cette section, vous ne manquerez que quelques détails techniques).

Une boucle for-of commence par appeler la méthode Symbol.iterator() de la collection utilisée. Ceci renvoie un nouvel itérateur qui peut être n’importe quel objet possédant une méthode .next() ; la boucle for-of appellera cette méthode de façon répétitive, une fois pour chaque itération. Par exemple, voici l’itérateur le plus simple auquel je peux penser :

var itérateurPleinDeZéros = {
   [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};

À chaque appel de la méthode .next(), le même résultat sera renvoyé et communiquera les informations suivantes à la boucle for-of :

  1. nous n’avons pas encore terminé les itérations ;
  2. la prochaine valeur est 0.

Cela signifie que la boucle for (valeur of itérateurPleinDeZéros) {} sera une boucle infinie. Évidemment, un itérateur utilisé dans du code ne sera pas aussi trivial.

Ce concept d’itérateur, avec les propriétés .done et .value, est légèrement différente de celui utilisé dans les autres langages. En Java, les itérateurs possèdent des méthodes séparées .hasNext() et .next(). En Python, ils possèdent une seule méthode .next() qui appelle StopIteration lorsqu’il n’y a plus de valeur. Fondamentalement, ces trois conceptions renvoient les mêmes informations.

Un itérateur peut également implémenter les méthodes optionnelles .return() et .throw(exc). La boucle for-of appelle la méthode .return() si la boucle est interrompue de façon prématurée, à cause d’une exception, d’une instruction break ou return.

Un itérateur peut également implémenter la méthode return() s’il est nécessaire de procéder à un nettoyage ou de libérer des ressources utilisées. La plupart des objets itérateurs n’auront pas à implémenter cette méthode. La méthode throw(exc) est un cas encore plus spécifique : les boucles for-of ne les appellent jamais, on en parlera la semaine prochaine.

Maintenant que nous connaissons tous les détails, nous pouvons nous intéresser à une boucle for-of simple et la ré-écrire avec les appels des méthodes sous-jacentes.

Commençons avec la boucle for-of :

for (VAR of ITERABLE) {
  INSTRUCTIONS
}

Voici un équivalent, un peu brut, utilisant les méthodes sous-jacentes ainsi que quelques variables temporaires :

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!result.done) {
  VAR = result.value;
  INSTRUCTIONS
  $result = $iterator.next();
}

Ce code n’illustre pas l’utilisation de la méthode .return(). On pourrait l’ajouter mais je pense que cela ajouterait plus de confusion. Les boucles for-of sont simples à utiliser, malgré cela, il se passe beaucoup de choses en arrière-plan.

Quand puis-je commencer à utiliser cette fonctionnalité ?

Les boucles for-of sont supportées par les versions actuelles de Firefox. Elles sont supportés dans Chrome sous réserve d’activer l’option « Activer la fonctionnalité expérimentale JavaScript » accessible via l’URL chrome://flags. Cela fonctionne également dans le prochain navigateur Edge (nom de code « Spartan ») de Microsoft. En revanche, cette fonctionnalité n’est pas disponible dans les différentes versions d’Internet Explorer. Si vous souhaitez utiliser cette nouvelle syntaxe et que devez prendre en compte IE et Safari, vous pouvez utiliser un compilateur tel que Babel ou Traceur de Google pour traduire votre code ES6 en code ES5, plus largement compatible avec les anciens navigateurs.

Côté serveur, pas besoin d’installer un compilateur - vous pouvez utiliser les boucles for-of dès aujourd’hui avec io.js (et avec Node en utilisant l’option --harmony)

(Mise à jour : j’ai oublié de mentionner que for-of est désactivé par défaut dans Chrome. Merci à Oleg d’avoir signalé cet oubli dans les les commentaires.) (NdT : voir ce commentaire).

{done: true}

Whaou !

Bon, c’est tout pour aujourd’hui, mais nous n’en n’avons toujours pas fini avec la boucle for-of.

Il y a encore une nouvelle sorte d’objet apparue avec ES6 et qui fonctionne très bien avec les boucles for-of. Je n’en ai pas parlé car c’est le sujet de l’article de la semaine prochaine. Je pense que cette nouvelle fonctionnalité est la plus magique d’ES6. Si vous ne l’avez jamais utilisé dans des langages comme Python et C#, vous serez vraisemblablement déstabilisé au début. Ce sera la manière la plus simple d’écrire un itérateur, un atout pour le refactoring et cela pourrait changer la façon d’écrire du code asynchrone tant au niveau du navigateur que du serveur.

Rejoignez-nous donc la semaine prochaine pour une étude approfondie des générateurs dans ES6.