Kergoz Panic

par
 
Watilin
le
 
31/01/2017 à 23:18
catégorie
 
Informatique

forEach et les collections DOM

Aujourd'hui je voulais écrire un article assez court pour vous parler d'une nouveauté du langage JavaScript : la boucle for..of. Mais après quelques tests, il me semble que cette technologie n'est pas encore assez mûre, alors à la place je vais vous parler de la méthode forEach et comment elle peut nous simplifier la vie quand on manipule des collections d'éléments.

Mise à jour : nous sommes à présent en janvier 2017 et la boucle for..of commence à avoir un support qui, bien qu’insuffisant pour une utilisation en production (voir ce tableau de compatibilité – attention, la page est assez longue à charger), permet déjà de faire des expérimentations. J’ai rajouté une section à ce propos à la fin de cet article.

Le problème de la boucle for traditionnelle

Si vous êtes développeur JavaScript, vous vous êtes certainement heurté au moins une fois au problème des collections d'éléments du DOM : ça ressemble à un tableau, mais ce n'est pas un tableau. En guise d'exemple tout le long de cet article, je supposerai que vous voulez faire une opération sur tous les liens <a> de votre page.

var liens = document.querySelectorAll("a");

Au fait, si vous ne connaissez pas querySelectorAll, c'est le moment de se mettre à la page ;)

forEach c'est bon, mangez-en

Développeur chevronné, vous avez l'habitude d'utiliser la méthode forEach des tableaux, parce que ça permet d'isoler les variables de boucle, parce que ça colle plus avec la philosophie fonctionnelle du langage, et tout simplement parce que vous trouvez ça plus élégant. Avec un tableau classique, vous feriez comme ceci :

tableau.forEach(function(item) {
  faireQuelqueChoseAvec(item);
});

Pas de forEach au dîner ce soir

Mais voilà, drame : liens n'est pas un tableau, adieu la méthode forEach appréciée. Vous voilà obligé de revenir à la bonne vieille boucle for.

for (var i = 0; i < liens.length; i++) {
  faireQuelqueChoseAvec(liens[i]);
}

La boucle for classique nous vient directement de la grande famille Java/C, la famille des langages impératifs.

Attardons-nous un peu sur la rivalité entre les langages impératifs et les langages fonctionnels. En général, on connaît mieux les langages impératifs, car comme je viens de le dire, Java et C en font partie. Ces langages se focalisent sur les instructions et leur exécution séquentielle. Les choses doivent se faire dans l'ordre. Le fondement théorique de cette famille, c'est la machine de Turing.

Les langages fonctionnels sont un peu plus abstraits, et un peu moins connus. Lisp, Scheme et Haskell en font partie, ainsi que Caml. Dans cette famille, on ne fait pas de boucles, on préfère écrire des fonctions récursives qui permettent de parcourir les structures de données. Cette famille de langage est basée sur la théorie du lambda-calcul.

JavaScript a eu une enfance difficile, car il a hésité, sans arriver à se décider, entre la famille impérative et la famille fonctionnelle. Conçu en moins d'une semaine par Brendan Eich, dans le contexte d'un partenariat commercial entre Netscape et Sun, il a reçu ce qui était considéré comme le plus attrayant des deux familles à l'époque. Ainsi, son système de portée des variables a été conçu dans l'inspiration des langages fonctionnels, mais il a reçu un jeu d'instructions séquentielles comme les langages impératifs. Vous allez voir que cette hybridation n'est pas toujours à son avantage.

La boucle for est une structure typique des langages impératifs. C'est la bonne vielle boucle, bien franche, bien rustique, que tout le monde connaît. Le problème c'est qu'il y a eu, dès le départ, une erreur dans sa conception quand elle a été introduite dans JavaScript : elle n'isole pas la variable de boucle. i se balade librement dans le bloc supérieur. À première vue ça ne pose pas de problème et ça ne fait que râler les puristes… Mais examinons ce cas assez fréquent :

for (var i = 0; i < liens.length; i++) {
  liens[i].onclick = function() {
    alert("Vous avez cliqué sur le lien " + i);
  };
}

Le point important de cet exemple est que je crée une fonction à chaque itération de la boucle (il y aura autant de fonctions qu'il y a de liens dans la collection), et que ces fonctions utilisent la variable de boucle i.

Difficile de voir où est le problème à première vue. En réalité, les fonctions onclick créées à chaque tour de boucle font référence à la variable i qui se trouve à l'extérieur de ces fonctions. Le mécanisme des closures est impliqué.

Au moment où la fonction est invoquée, quand l'utilisateur clique sur un lien, quelle est la valeur de i ? Si elle n'a pas été modifiée dans la suite du script, elle est égale à liens.length, sa valeur à la fin de la boucle. S'il y avait 6 liens dans la page, l'utilisateur recevra le message vous avez cliqué sur le lien 6 quel que soit le lien sur lequel il a cliqué.

Une solution moche

Je vous entends déjà poser la question : comment faire pour avoir la bonne valeur de i dans chaque fonction onclick ? La solution n'est pas très élégante :

for (var i = 0; i < liens.length; i++) {
  (function(ii) {
    liens[ii].onclick = function() {
      alert("Vous avez cliqué sur le lien " + ii);
    };
  }(i));
}

Le principe est de créer une fonction anonyme qui va être immédiatement appelée : (function() { ... }()). Les anglophones appellent ça une IIFE, mais je préfère appeler ça une fonction éphémère, je trouve ça plus joli :) On passe donc à cette fonction éphémère la valeur de i à chaque itération. J'ai renommé son paramètre en ii pour rendre les choses plus claires. ii reçoit donc la valeur de i à chaque tour, et comme ii est une variable locale, on est sûr qu'elle a la bonne valeur au moment où la fonction éphémère est appelée.

Il n'est pas rare de voir, parmi les développeurs qui utilisent cette technique, que le paramètre de la fonction s'appelle aussi i. On a alors un code qui ressemble à ça :

for (var i = 0; i < liens.length; i++) {
  (function(i) {
    liens[i].onclick = function() {
      alert("Vous avez cliqué sur le lien " + i);
    };
  }(i));
}

Techniquement ça marche toujours. C'est tout à fait valide en JavaScript : vous pouvez créer une variable locale qui a le même nom qu'une variable extérieure ; la variable extérieure est masquée, elle n'est simplement pas accessible depuis l'intérieur de la fonction. Ici, le paramètre i masque la variable de boucle i, qui est extérieure à la fonction éphémère.

Comme vous le voyez cette technique n'est pas élégante : elle augmente la quantité de code, et en plus elle est difficile à comprendre quand on ne l'a jamais rencontrée.

Pour résumer, la boucle for traditionnelle crée des complications quand on veut utiliser des fonctions à l'intérieur, et ces complications peuvent devenir un réel problème assez souvent. Heureusement, JavaScript a plus d'une corde à son arc.

Appeler forEach sur des non-tableaux

La souplesse de JavaScript permet d'invoquer la méthode d'un objet sur un autre objet. En général on évite de le faire, car ça peut vite devenir cauchemardesque. Mais dans certains cas c'est bien pratique, et la méthode forEach est un parfait exemple.

Alors comment faire ? Récapitulons : nous avons, d'un côté, une collection liens qui n'est pas un tableau ; et de l'autre, une méthode forEach que l'on trouve sur les tableaux.

Nous allons utiliser call : c'est une méthode de fonction (oui, en JS, les fonctions ont des méthodes) qui permet de changer le contexte (l'objet this) de la fonction. En l'appellant sur forEach, on peut lui donner liens comme contexte, et ceci nous permet d'appeler forEach comme si c'était une méthode de liens !

[].forEach.call(liens, function(lien) {
  faireQuelqueChoseAvec(lien);
});

Voilà, on y est arrivé ! Un petit détail : nous avons tout de même besoin d'un tableau au départ, pour avoir une méthode forEach sous la main. Dans mon exemple j'ai pris un tableau vide [], mais c'est un peu dommage de créer un nouveau tableau à chaque fois qu'on a besoin de forEach. Pourquoi ne pas utiliser directement le prototype de tous les tableaux, qui est toujours présent ?

Array.prototype.forEach.call(liens, ...); // le reste ne change pas

D'un point de vue performance, c'est mieux, mais niveau code, c'est moins beau. Par chance, les navigateurs les plus cools implémentent ce qu'ils appellent les méthodes génériques :

Array.forEach(liens, ...);

Ça c'est vraiment mieux, car plus besoin de tricher avec call, et on utilise quelque chose de bien documenté, donc ceux qui lisent le code sont censés comprendre.

Il y a des navigateurs moins cools (IE11 pour n'en citer qu'un) qui ne supportent pas les méthodes génériques, mais heureusement c'est une lacune très facile à combler :)

if (!Array.forEach) {
  Array.forEach = function(collection, callback, context) {
    Array.prototype.forEach.call(collection, callback, context);
  };
}

Oui, forEach a un paramètre optionnel context, qui permet de dire quel doit être le this dans la fonction de callback. On ne s'en sert pas souvent mais il existe, alors autant le prendre en charge, ça ne coûte pas cher.

Mise à jour : en vérifiant le lien je viens de voir que les méthodes génériques sont dépréciées et vont apparemment être retirées dans un avenir proche. C’est un peu triste, mais après tout, elles n’étaient pas indispensables. On pourra toujours utiliser le polyfill.

Transformer une collection en tableau

On a parfois besoin, pour une raison ou une autre, de transformer une collection en tableau, et le garder au chaud pour plus tard. Je vais donner deux façons de faire, mais il y en a d'autres.

En utilisant slice

C'est une méthode qui vous permet de travailler en toute tranquilité sur des collections, même sous les plus vieux navigateurs, car slice a toujours fait partie de JavaScript. L'astuce vient du fait que si on appelle slice avec 0 et en omettant le second paramètre, il renvoie le tableau entier.

[1, 2, 3].slice(0) // => [1, 2, 3]

Grâce à Array.prototype comme je l'ai fait tout à l'heure avec forEach, on peut forcer une collection à se transformer en tableau, d'une façon qui marche même sous IE6, et tout ça en une seule ligne de code !

var tabLiens = Array.prototype.slice.call(liens, 0);

En utilisant map

C'est la technique que j'utilisais avant de connaître celle de slice. Elle est un peu moins concise, mais je pense qu'elle peut être intéressante à connaître. Le principe de map est de transformer chaque élément du tableau, et de renvoyer un nouveau tableau contenant les éléments transformés. En utilisant une fonction qui renvoie un élément sans le transformer, on obtient une simple copie de ce tableau. Et en utilisant Array.prototype on peut transformer une collection en tableau.

Array.prototype.map.call(liens, function(lien) { return lien; });

En utilisant Array.from

Méthode apparue avec la spécification ECMAScript de 2015 (ES6), la méthode Array.from existe précisément dans le but de transformer un objet itérable en tableau. C’est la seule méthode générique, à l’heure où j’écris ce paragraphe (début 2017), qui est standard et qui n’est donc pas menacée de disparition prochaine.

On peut l’utiliser dès aujourd’hui sur les navigateurs qui la prennent en charge, ou en faire facilement un polyfill au moyen des méthodes slice ou map que vous venez de voir. Je vous laisse l’écriture de ce polyfill en exercice ;)

Quelques mots pour finir

Avec forEach, JavaScript renoue avec ses racines de langage fonctionnel. Cette méthode n'a pas toujours existé ; chez Microsoft elle n'est implémentée qu'à partir d'IE9. Heureusement il est possible de l'émuler avec un polyfill, à l'instar des autres méthodes d'itération : map, reduce, filter, every et sans doute d'autres que j'oublie.

Chacune de ces méthodes a son utilité, et les connaître vous permet de mettre en œuvre des solutions créatives et élégantes. Grâce à cet article, vous connaissez maintenant la façon de les appliquer aux collections DOM. Ajoutez à ça les polyfills qu'on peut trouver partout sur le Net (il existe un polyfill de querySelector pour IE7 qui tient en 26 lignes !), et vous avez un véritable petit framework de parcours du DOM, puissant, léger et compatible.

Mise à jour : itérer en 2017

La boucle for..of

for..of apporte à la fois l’élégance et la performance que les autres solutions abordées au long de cet article échouent à atteindre. On peut enfin écrire une boucle sans lourdeurs syntaxiques, et sans recourir à des invocations de fonctions supplémentaires.

for (var lien of liens) {
  faireQuelqueChoseAvec(lien);
}

Je crois que je n’ai pas grand chose à ajouter, le code s’explique de lui-même.

D’un point de vue logique, c’est exactement équivalent à la solution forEach présentée au début de cet article. D’un point de vue performances, en revanche, je suis forcé d’admettre que la solution impérative – à savoir for..of – est plus efficace que son pendant fonctionnel. De nombreux tests JSPerf l’ont prouvé. La nature interprétée de JavaScript fait que chaque invocation de fonction présente une léger surcoût en temps d’exécution, et, avec forEach, la fonction passée est appelée pour chaque élément de la collection, cumulant le temps supplémentaire à chaque tour. Moralité : usez et abusez de for..of !

Cette nouvelle boucle apporte indirectement un autre avantage : faisant partie de la spécification ES6, elle côtoie le nouveau mot-clé let, semblable à var, qui permet d’isoler la portée d’une variable au bloc courant. Ainsi, il est désormais possible de faire des boucles for dont les variables ne fuient pas à l’extérieur !

for (let lien of liens) {
  faireQuelqueChoseAvec(lien);
  // ici, lien est défini
}
// ici, lien est indéfini

On notera tout de même qu’en l’absence d’indice d’itération i, on n’a pas réglé le problème que nous avions tout à l’heure avec le i qui avait la valeur de fin de boucle partout. Pour ce genre de cas je conseillerai de recourir à forEach ou, plus efficace, de faire de la délégation d’évènement si les conditions le permettent. Ça fera peut-être l’objet d’un prochain article :)

Quelques mots pour finir (encore)

En 2017, nous voyons progresser for..of dans l’écosystème JavaScript. Cependant, son support n’est pas encore suffisamment répandu – notamment, il n’est pas disponible sous IE11 ou Android. Le fait qu’il s’agit d’une nouvelle construction du langage, et pas simplement une fonction, fait qu’il est impossible de l’émuler au moyen d’un polyfill. Pour l’heure, il faut donc nous contenter de moyens d’itération moins récents, ou bien prendre le risque de rendre notre code inopérant pour une partie des internautes…

Commentaires

Aucun commentaire n’a encore été posté sur cet article.

Ajouter un commentaire

Anti-bot provisoire