Pour les Jedis, JavaScript, épisode II : L’attaque des Closures (partie 2)

Cet article fait partie de la série “Pour les Jedis, JavaScript” d’articles consacrés à JavaScript. Si ce n’est pas déjà fait, veuillez lire les épisodes précédents.
  1. Pour les Jedis, JavaScript, épisode I : Au coeur des Fonctions (1 et 2)
  2. Pour les Jedis, JavaScript, épisode II : L’attaque des Closures (1 et 2)
  3. Pour les Jedis, JavaScript, épisode III : La revanche des Prototypes (1 et 2)

Découvrons la suite de certains cas d’usages des Closures

Fonction de callback et timers

Un autre cas d’usage des Closures, est l’utilisation des fonctions de callback ou des timers. Dans ces deux cas, une fonction sera appelée de manière asynchrone à un moment donné, et dans laquelle nous avons — souvent — besoin d’accéder à des informations se trouvant en dehors. Prenons l’exemple suivant :

Ici, nous avons une DIV dans laquelle nous venons insérer du texte chargé depuis un serveur distant, via l’API fetch. Pour cela, nous avons référencé la DIV en dehors de la fonction de callback du clique. Ainsi, lorsque le clique est dispatché (exécuté) par le navigateur et que la fonction est invoquée, elle a accès à la variable content, référençant la DIV.

La plupart des développeurs JavaScript (vanilla) sont familiers avec ce type de code. Et pourtant, si JavaScript nous permet d’écrire ce genre de code, c’est bien grâce aux Closures.

Passons maintenant à un autre exemple plus intéressant, faisant intervenir des timers cette fois-ci. Les timers étaient (et sont) souvent utilisés par les librairies JavaScript pour réaliser des effets ou des animations :

Vous pouvez trouver une version interactive ci-dessous sur StackBlitz :

Ce qui est intéressant dans cet exemple, c’est l’utilisation d’une seule fonction anonyme dans setInterval pour réaliser l’animation ; cette fonction de callback a accès aux trois variables : content la référence vers l’élément DOM, le compteur tick et la référence timer vers la fonction du timer. Ces trois variables contrôlant l’état de l’animation ne doivent pas être mises dans l’espace global. La raison est simple : si vous tentez d’animer plus d’un élément DOM, vous aller devoir maintenir N groupes des ces trois variables, un groupe par animation ; dit autrement, en ayant que trois variables globales pour les N animations, je vous laisse donc imaginer les dégâts.

NOTE: De nos jours, la plupart des navigateurs modernes supportent les APIs requestAnimationFrame et Web Animation capable de gérer les animations complexes de façon performante. L’utilisation des Timers n’est donc plus recommandée pour gérer les animations !

Grâce aux Closures, nous pouvons déclarer ces variables au sein de la fonction animateRight, et nous pouvons compter sur les Closures pour les rendre accessible aux invocations des fonctions anonymes, de chaque animation.

Nous pouvons illustrer cela comme suit :

Chaque animation reçoit donc une encapsulation dynamique de l’état de son contexte d’invocation. Par dynamique, j’entends par cela que la fonction peut non seulement accéder à ces variables (qui existent dans le contexte d’invocation) mais également les modifier, et cela pendant toute la durée de vie de la closure. Et ce n’est pas tout !

En plus d’accéder et de modifier les variable du contexte d’invocation, il est possible de forcer un contexte en particulier grâce à la fonction Function.prototype.bind(), comme discuté dans l’épisode 1 sur les fonctions.

Fonction partielle

L’application partielle d’une fonction est une technique très intéressante. Cela consiste à invoquer une fonction qui, une fois exécutée, retourne à son tour une autre fonction, destinée à être exécutée ultérieurement.

Dit simplement, on exécute une fonction, qui fait une partie des traitements puis retourne une fonction qui, quand elle sera exécutée, fera la suite des traitements. Plus généralement, le rôle de la première fonction est de configurer/préparer les paramètres de la seconde fonction retournée. En mathématiques, cette technique est appelée Currying et nous pouvons retrouver ce concept dans la plupart des langages dit fonctionnels (Haskell, Scheme, Scala, Python…).

Bon, passons aux exemples.

Cet exemple est simple mais il illustre très bien l’utilisation des fonctions partielles. Explications…

Nous avons créé une fonction curry() qui prend en paramètre une fonction (en première position) et une suite de paramètres (le reste des paramètres). La fonction curry(), accède à la liste des paramètres grâce à la décomposition, le premier paramètre fn qui a été nommé explicitement, puis le reste des paramètres; puis retourne une fonction qui va être exécutée ultérieurement.

Cette fonction retournée a pour mission d’invoquer la fonction fn — via un apply — qui a été passée à la fonction curry() en lui fournissant la liste des paramètres précédemment renseignés. Cette dernière étape est rendue possible grâce aux Closures.

Un exemple d’utilisation de cette fonction curry() pourrait être le suivant :

Grâce à la fonction curry() nous avons créé une fonction delay() que nous avons “configuré” comme étant une référence vers la fonction setTimeout avec un délai d’une seconde. Grosso modo, nous avons maintenant une fonction qui nous permet d’exécuter une action avec une seconde de délai. Super utile, n’est-ce pas ?

Voici l’exemple au complet :

IIFE (Immediate Invocation Function Expression)

Les Closures sont également utilisées au sein de nombreux patrons de conception (Design Pattern) très présents en JavaScript. Parmi ces Design Patterns, nous pouvons citer le patron IIFE (Immediate Invocation Function Expression), en français cela donne : les fonctions auto-invoquées. Ce patron est principalement utilisé pour encapsuler des objets ou du comportement dans des modules JavaScript. Les IIFE sont encore assez répandu dans le code des librairies et frameworks écrits en ES5. Les choses ont changé avec l’arrivé de ECMA2015 et les ES Modules.

Étudions tout de même les bases de ce patron de conception que l’on pourrait réduire à cette ligne de code :

Analysons cette petite ligne de code… Tout d’abord, ignorons le contenu du premier ensemble de parenthèses ainsi que le commentaire, pour ne garder que ceci :(…)()

Si vous vous rappelez ce que nous avons dit lors du premier épisode : JavaScript traite les fonctions comme des fonctions de première ordre, c’est-à-dire que nous pouvons référencer une fonction depuis une variable, et nous pouvons invoquer cette variable, comme ceci (partie A) :

En JavaScript, les fonctions sont considérées comme étant des expressions ; et nous pouvons exécuter ces expressions en utilisant l’opérateur () —les parenthèses. Mais là où les choses peuvent paraître un peu confuses, les parenthèses peuvent également être utilisées pour délimiter les expressions. C’est-à-dire que le code suivant est tout à fait valide (partie B) :

Du coup, nous pouvons omettre la déclaration de la variable, et passer par une fonction anonyme (partie C) :

Ajoutons un peu de code :

Dans l’exemple ci-dessus, nous avons imbriqué deux IIFE, ajouté un peu de Closures, et fourni un paramètre à la première IIFE.

Le résultat de cet exemple est une expression qui :

  1. Crée une première instance de fonction ;
  2. Exécute la fonction ;
  3. Crée une seconde instance de fonction ;
  4. Exécute cette seconde fonction et retourne le résultat ;
  5. Se débarrasse de cette seconde fonction (car elle n’est référencée nulle part) ;
  6. Retourne le résultat ;
  7. Se débarrasse de la première fonction (car elle n’est référencée nulle part non plus).

De plus, grâce aux Closures, la seconde fonction accède à tout le contexte de la première fonction, ainsi qu’à la liste des paramètres.

On se rend compte du coup, que cette construction assez simple peut s’avérer très puissante et très utile, dans certains cas. Voyons un autre exemple.

Il arrive des fois où vous devez attacher des évènements à plusieurs éléments du DOM. Bon, normalement si tel est le cas, je vous recommande de passer par le patron de Délégation d’Événements (Event Delegation). Mais si vous n’avez pas le choix, vous seriez surement tenter de faire quelque chose du style :

Dans l’exemple précédent, imaginez que vous avons 8 boutons dans votre page. Vous vous attendez donc à ce que vous ayez un message s’afficher avec l’index de l’élément en question. En cliquant sur le bouton #3, vous voulez avoir l’index 3 s’afficher. Mais au lieu de cela, vous avez toujours le dernier index 9.

Nous rencontrons ici un problème typique lié au Closures et les boucles. Ce problème concerne dans notre cas, la variable passée à la Closure (la variable i). Le fait est que cette variable est mise à jour à chaque itération. Aussi, chaque fonction — ou event handler — du addEventListener garde une référence vers chaque variable i passée par Closure. Ce qui signifie donc que chaque event handler affichera toujours la dernière valeur stockée dans la variable i. Beaucoup de développeurs débutants en JavaScript tombent dans ce piège. Je me souviens de ma première fois !

Ne déclarez jamais de fonctions dans les boucles, si vous n’avez pas le choix, passez par une IIFE.

Maintenant que je vous ai présenté le problème, parlons de la solution. Vous allez être étonnés d’apprendre que pour résoudre ce problème introduit à la base par une Closure, nous allons avoir besoin d’une autre Closures (plus une IIFE). Comment on le dit, on va combattre le feu par le feu !

En utilisant une IIFE en tant que corps de la boucle, et en lui passant l’index courant, représenté par la variable i, en tant que paramètre, nous créons un contexte isolé dans lequel chaque variable i est différente.

Voilà donc comment, grâce aux Closures et IIFE, vous pouvez contrôler le contexte des variables et des valeurs.

Résumé

Dans ce second épisode (partie 1 et 2), nous avons appris et compris comment les Closures — un des concept majeur de la programmation fonctionnelle — sont implémentées en JavaScript. J’espère que grâce à cet épisode vous aurez suffisamment de bagage pour votre quête ultime pour devenir un Jedi en JavaScript.

Dans notre prochain épisode, nous nous attaquerons à un autre concept de JavaScript : La programmation Orientée Objet avec les Prototypes. Ce dernier concept exploite le premier aspect que nous avons découvert, celui des fonctions du premier ordre, et le second concept, les fermetures. En tant que future Jedi en JavaScript, maitriser la POO en JavaScript est une étape cruciale dans votre quête, et il est de votre devoir d’accomplir votre mission.

Que le JS soit avec vous…