javascript-pour-les-jedis
JavaScript

JavaScript pour les Jedis, épisode II : L’attaque des closures

Dans ce second épisode consacré à l’apprentissage des fondamentaux de JavaScript pour les Jedis, nous allons aborder un autre aspect offert par ce langage : les fermetures (ou closures en anglais). Nous allons d’abord tenter de définir et comprendre les closures, ensuite nous verrons comment ces closures facilitent nettement le développement JavaScript, en les exploitant pour résoudre des problèmes courants liés à la porté et aux contextes des fonctions.

Etroitement liées aux fonctions, dont nous avons longuement discuté lors du premier épisode, les closures sont un des trois piliers de JavaScript. Pour rappel, ces trois piliers étant : les objets, les fonctions et les closures.

Historiquement, les closures étaient exclusivement réservées aux languages fonctionnels “purs” (par exemple : Haskell), et c’est donc très encourageant de les avoir dans un language grand publique, comme JavaScript. Ne soyez pas très surpris de voir les closures dans la plupart des librairies et frameworks JavaScript ; dites-vous que si les JavaScript Jedis sont très friands des closures c’est parce que ces dernières permettent de simplifier de manière drastique des opérations très complexes.

Tentons donc de définir ce que c’est les closures.

Comment fonctionnent les closures ?

Si nous devions définir les closures en une phrase : une closure est un contexte crée par la déclaration d’une fonction, et qui permet à cette dite fonction d’accéder et manipuler des variables se trouvant en dehors de la portée de cette fonction. Ca va ? C’est assez clair ? Disons qu’une closure permet à une fonction foo d’accéder à toutes les variables et fonctions qui sont dans le contexte de déclaration (et non d’invocation) de cette fonction foo.

Prenons un simple exemple :

var jedi = "JavaScript";
function foo(){
    console.log(jedi); // JavaScript
}
foo();

Dans cet exemple, nous avons déclaré une variable jedi, et une fonction foo dans le même contexte — dans ce cas le contexte globale. Lorsque nous exécutons la fonction foo , celle-ci a bien accès à la variable jedi. Je parie que vous avez écrit ce genre de code une dizaine de fois sans vous rendre compte que vous étiez en train de manipuler des closures !

Si vous pensez que cet exemple était trop simple, sûrement parce que vous avez remarqué que la variable et la fonction sont déclarées dans le contexte globale qui, tant que la page est chargée, est toujours accessible et ne change pas. Prenons un exemple un peu plus intéressant.

var luke = "luke";
var jedi;
function foo(){
    var vador = "je suis ton père";
    function bar(){
        console.log(luke, vador);
    }
    jedi = bar;
}
foo();
jedi();

Analysons le comportement de la fonction bar car il est plus intéressant.

Nous exécutons la sous-fonction bar en différé, après l’invocation de la fonction foo , via la copie de la référence de bar vers jedi . Remarquez que lorsque cette sous-fonction est exécutée, le contexte crée par la fonction foo n’est plus disponible, ce qui voudrait dire que la variable vador n’est plus accessible ?!

Sauriez-vous donc me dire quel serait le résultat de console.log(luke, vador); ?

Pensez-vous que la réponse est : luke undefined ? Alors vous serez surpris si je vous dis que non. Le résultat est bel est bien luke je suis ton père . Quelle magie a donc permis à la variable vador d’être toujours accessible, même après la finalisation du contexte crée par la fonction foo ? La réponse est bien sûr, les closures.

Les closures créent donc une sorte de “bulle” avec toutes les variables et fonctions — ainsi que la liste de leurs arguments — qui sont dans le contexte de la fonction au moment de sa déclaration, ainsi cette dernière aura tout ce dont elle aura besoin lors de son invocation. Cela permet donc de sécuriser ces variables et fonctions en leur évitant d’être détruites par le ramasse miette.

closures-fermetures-bulle

A noter tout de même que cette bulle ou structure n’est pas un objet JS auquel on peut accéder ou que l’on peut inspecter ou déboguer aussi facilement. Cependant, Google Chrome a récemment ajouté une toute petite feature dans le dev tools permettant l’inspection de ces closures.

closures-fermetures-chrome-dev-tools

L’utilisation des closures présente tout de même des inconvénients : il faut bien stocker toutes ces informations en mémoire. Rappelez-vous que chaque fonction ayant accès à des informations via des closures, doit vivre avec cette bulle que l’on pourrait qualifier de “boulet”. Toutes ses informations doivent être mise en mémoire et y rester durant toute la vie de la fonction. Un conseil donc : utilisez les closures avec modération, et uniquement là où il y en a besoin !

Cas d’usages

Voici une liste des différents cas d’usage illustrant l’utilisation des closures…

Variables privées (encapsulation)

Une des utilisations les plus répandues des closures est l’encapsulation d’information en tant que “variables privées”, pour limiter la portée de ces variables. La Programmation Orientée Objet en JavaScript ne permet pas d’avoir des variables privées : des propriétés d’un objet non accessible depuis l’extérieur. Mais en utilisant les closures, nous pouvons reproduire ce comportement. Voyons cela en code.

function Jedi(){
    var jedi = "";
    this.luke = function(){
        jedi = "Luke";
        return this;
    }
    this.imYourFather = function(){
        return jedi + ", Je suis ton père";
    };
};
var jedi = new Jedi();
jedi.luke().imYourFather(); // Luke, Je suis ton père

Dans cet exemple, nous avons définit une variable jedi dans le constructeur. Comme nous l’avons vu dans le premier épisode, JavaScript limite la portée de cette variable à la fonction servant de constructeur. Afin de pouvoir accéder à cette variable depuis l’extérieur, nous avons définit deux méthodes permettant de modifier et lire cette variable.

Après invocation du constructeur, nous invoquons les deux méthodes et en résultat, nous avons bien le comportement attendu.

closures-fermetures-variable-privée-chrome-dev-tools

Nous avons donc encapsuler la variable en limitant sa portée et cela grâce aux closures. Voici ce que cela donne avec notre schéma de bulle :

closures-fermetures-varibale-privée

Ceci était donc un simple et rapide aperçu de ce que l’on peut faire avec les closure et l’orienté objet. Nous explorerons en detail le monde du JavaScript Orienté Objet dans le prochain épisode.

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 :

<div id="content"></div>
<button id="load">Vador says...</button>
<script>
//
    var content = document.querySelector('#content');
    var click = function(){
        content.innerHTML = 'Luke, ...';
        var xhr = new XMLHttpRequest();
        xhr.open('GET', '/api/users/luke', false);
        xhr.onload = function(data){
            // data.response : ... I'm your father!
            content.innerHTML = data.response;
        };
        xhr.send();
    };
    document.querySelector('#load').addEventListener('click', click);
//
</script>

Ici, nous avons une DIV dans laquelle nous venons insérer du texte chargé depuis un serveur distant, via AJAX.  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 plus part des développeurs JavaScript 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 sont souvent utilisés par les librairies JavaScript pour réaliser des effets ou des animations.

<div id="content"> ♥ </div>
<button id="animateRight">Animate Right</button>
<script>
//
    var animateRight = function(){
        var tick = 0;
        var content = document.querySelector('#content');
        var timer = setInterval(function(){
            if(tick < 200){
                content.style.left = tick + 'px';
                tick += 1;
            }
            else {
                clearInterval(timer);
            }
        }, 15);
    };
    var click = function(){
        animateRight();
    };
    document.querySelector('#animateRight').addEventListener('click', click); 
// 
</script>

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.

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 :

closures-fermetures-fonction-anonyme

 

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 secondes fonction retournée. En mathématiques, cette technique est appelée “currying” et nous pouvons retrouver ce concept dans la plus part des languages “fonctionnels” (Haskell, Scheme, Scala, Python…).

Bon, passons aux exemples.

function curry(fn) {
  var restArgs = Array.prototype.slice.call(arguments, 1);
  return function() {
    var arg = Array.prototype.slice.call(arguments);
    return fn.apply(null, arg.concat(restArgs));
  };
};

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

Nous avons crée 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 (en excluant le premier, le paramètre fn qui a été nommé explicitement) 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 :

var delay = curry(setTimeout, 1000);
delay(function() {
  alert("POWNED!!");
});

Grâce à la fonction curry() nous avons crée 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. Très utile, non ? ^^

IIFE (Immediate Invocation Function Expression)

Les closures sont également utilisées au sein de nombreux patrons de conception très présents en JavaScript. Parmi ces patrons, 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.

Etudions d’abord les bases de ce patron qu’on pourrait réduire à cette ligne de code :

(function(){})()

Analysons cette petite ligne de code… Tout d’abord, ignorons le contenu du premier ensemble de parenthèses, 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 :

var luke = function(){};
var jedi = luke();

En JavaScript, les fonctions sont considérées comme étant des expressions ; et nous pouvons exécuter ces expressions en utilisant l’opérateur () . 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 :

var luke = function(){};
var jedi = (luke)();

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

var jedi = (function(){})();

Ajoutons un peu de code :

var jedi = (function(what){

  var say = "I'm your "+what+"!";

  return (function(){
    return "Luke, "+say;
  })();

})("father");

console.log(jedi); // "Luke, I'm your father!"

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

Le résultat de cet exemple est une expression qui produit ceci, elle :

  • crée une première instance de fonction ;
  • exécute la fonction ;
  • crée une seconde instance de fonction ;
  • exécute cette seconde fonction et retourne le résultat ;
  • se débarrasse de cette seconde fonction (car elle n’est référencée nulle part) ;
  • retourne le résultat ;
  • 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 closure, 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 la délégation d’événements. Mais si vous n’avez pas le choix, vous seriez surement tenter de faire quelque chose du style :

var elements = document.querySelectorAll('button');
var len = elements.length; // 8 par exemple
for(var i=0; i<len; i+=1){
  elements[i].addEventListener('click', function(){
    alert(i+1)
  }, false);
}

Dans l’exemple précédent, imaginez que vous avons 8 boutons dans notre 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 fermetures et les boucles. Ce problème concerne dans notre cas, la variable passée à la fermeture (la variable i). Le fait est que cette variable est mise à jour à chaque itération. Aussi, chaque fonction — ou handler — du addEventListener garde une référence vers chaque variable i passée par closure. Ce qui signifie donc que chaque 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.

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 la closure, nous allons avoir besoin d’une autre closure (plus une IIFE).  Comment on le dit, on va combattre le feu par le feu ^^

var elements = document.querySelectorAll('button');
var len = elements.length; // 8 par exemple
for(var i=0; i<len; i+=1){
 (function(n){
    elements[n].addEventListener('click', function(){
      alert(n+1)
    }, false);
  })(i);
}

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 fermetures et IIFE, vous pouvez contrôler le contexte des variables et des valeurs.

Résumé

Dans ce second épisode, 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 article 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 prototype. 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 la force soit avec vous…

Standard