Dernière mise à jour :

Les closures

Closure est un terme qui revient très souvent dans la littérature ou les articles de blogs consacrés au JavaScript. Il a un caractère un peu magique, nappé de mystère, partagé par les seuls initiés… mais qu’en est-il réellement ? Quelles spécifications du langage se cachent dessous ? Dans quel cas cette forme de programmation trouve-t-elle son utilité voir sa nécessaire utilisation ?
A partir des spécifications formalisées par l’organisme ECMA International, nous allons étudier le fonctionnement d’une closure par l’exemple. Ensuite nous nous pencherons sur la façon dont le langage ECMAScript, dans sa version 5.1, gère la portée des variables, des fonctions, ainsi que ce que l’on nomme "name binding", à savoir l’association de valeurs et d’identifiants.

(Les sources sur GitHub >>)

Un exemple maladroit

Imaginons un développeur débutant et candide, qui souhaiterait animer et embellir sa page d’accueil en faisant apparaitre successivement des images. Il pourrait être tenté d’écrire le code suivant :

 Exemple 1

// Identification des éléments DOM

var div0 = document.getElementById('div1');

var div1 = document.getElementById('div2');

var div2 = document.getElementById('div3');

var div3 = document.getElementById('div4');

var div4 = document.getElementById('div5');

var nodes = [div0, div1, div2, div3, div4];

// Définition de la MAUVAISE solution

function show_my_pics (args) {

var i;

for (i = 0; i < 4; i++) {

setTimeout(function(){

args[i].style.visibility = 'visible';

console.log(i);

}, 500 * i)

}

}

show_my_pics(nodes);

Et là oups !, le résultat n’est pas du tout celui attendu. En effet contrairement à ce que pourrait penser notre développeur débutant, les valeurs de i ne sont pas celles que l’on pourrait attendre à première vue. Les fonctions dont l’exécution est mise en attente par setTimeout() gardent effectivement un accès à i bien que la fonction show_my_pics() ait terminé son exécution, mais de part la façon dont ECMAScript5 gère les variables , la valeur de i est celle qui a cours à la fin de l’exécution de la fonction soit ici 4 (voir la console pour s’en convaincre).
La solution passe donc par une autre structure de programmation et, vous l’aurez peut-être deviné, ce sont les closures.

Une solution pour notre développeur

Considérons le code suivant qui permet d’atteindre le résultat escompté :

 Exemple 2

// Identification des éléments DOM

var div0 = document.getElementById('div1');

var div1 = document.getElementById('div2');

var div2 = document.getElementById('div3');

var div3 = document.getElementById('div4');

var div4 = document.getElementById('div5');

var nodes = [div0, div1, div2, div3, div4];

// solution CORRECTE

var appear = function(args){

for(var i = 0; i<5; i++){

(function(key){

setTimeout(function(){

args[key].style.visibility = 'visible';

console.log(key);

}, 500 * key);

})(i);

}

}

appear(nodes);

Notre fonction appear() diffère de la fonction que nous avions appelé show_my_pics() par le fait que nous avons placé la fonction setTimeout() dans une IEF (Immediatly Executed Function) qui accepte un paramètre (key) et à laquelle nous passons la valeur de i comme argument.
Remarquons que dans cet exemple, à la fin de l’exécution de l’IEF, la variable key disparait pour le reste du programme. Pourtant elle reste disponible pour la fonction anonyme imbriquée, elle-même paramètre de la fonction setTimeout(), et qui est exécutée après le délai indiqué en 2éme argument.
Contrairement à l’exemple précédent chaque valeur de i est conservée dans la fonction en étant associée à key, elle y est « enfermée » pour ainsi dire… et oui, nous sommes face à une closure !
Voyons les processus et caractéristiques expliquant ce comportement.

Les spécifications du langage ECMAScript5

Généralités

Tout d’abord il faut noter que dans les spécifications ECMAScript5, les fonctions et le contexte dans lequel elles sont exécutées, sont modélisés sous forme d’objets. Ainsi lorsqu’une fonction est appelée un execution context est activé. Il s’agit d’un objet qui a au moins trois composants : ThisBinding, Lexical Environment et Variable Environment.

C’est ici ce que fait le Lexical Environment, lexical signifiant qu’il le fait en se basant sur la position des éléments dans le code du programme ECMAScript5. Lexical Environment et Variable Environment sont deux notions très proches et définir leur distinction complexifierait grandement cet article qui se veut plus pédagogique d’exhaustif. Aussi comme tous les deux sont des Lexical Environment j’utiliserais le terme générique Lexical Environment. Le Lexical Environment est composé de deux éléments :

Il est important de préciser que ces éléments sont purement des spécifications théoriques données par ECMA International pour le langage ECMAScript. Lors de l’implémentation concrète de ces spécifications, par exemple par Google avec son interpréteur V8 pour son navigateur Chrome, aucun élément de programmation ne permet d’accéder à ces objets. Jamais aucun programme JavaScript ne peut les manipuler directement.

Illustration avec notre premier exemple

Dans notre exemple 1 la fonction show_my_pics() est déclarée dans l’objet global donc window pour un navigateur. En simplifiant le Lexical Environment de départ se composerait donc de la façon suivante :

context.lexicalEnvironment = {

environmentRecord : .{ div1 : …, div2 : … nodes : […], show_my_pics : function…},

outer : null

} ;

show_my_pics.lexicalEnvironment = {

environmentRecord : .{ i : …, arguments :args,…},

outer : context.lexicalEnvironment

} ;

Premièrement, à l’exécution de show_my_pics(), un nouveau Lexical Environment est créé. Dans son Environment Record on trouvera { … arguments : nodes, …i : 0…}. A chaque tour de boucle la fonction setTimeout() est exécutée puis, après le délai défini, c’est la fonction passée en argument qui à son tour est exécutée. A chaque exécution le Lexical Environment correspondant va être créé, mais ce qu’il est important de noter, c’est que la variable i, dont nous avons besoin pour identifier l’élément sur lequel est appliqué le style visibility : visible (args[i]), ne s’y trouvera pas. En effet i n’est ni déclarée dans la fonction, ni passée en arguments. Pourtant notre fonction lui trouve une valeur…

C’est là que s’illustre un principe des Lexical Environments dans ECMAScript à savoir une relation d’héritage qui lie des Lexical Environment imbriqués. Cela signifie que si un élément n’est pas trouvé par l’interpréteur dans un l’Environment Record d’un Lexical Environment, il va le rechercher dans le Lexical Environment « parent » qui est référencé par la propriété outer et ainsi de suite jusqu’à le trouver. S’il n’est pas trouvé dans le contexte global (le plus haut étant window pour un navigateur) une erreur se produit. Ce mécanisme, dans son fonctionnement uniquement, est à rapprocher de la chaine des prototypes en JavaScript (voir : Javascript | MDN - Inheritance_and_the_prototype_chain).
Pour revenir à notre variable i, par le biais de la propriété outer du Lexical Environment de la fonction, sa valeur va être recherchée et trouvée dans l’Environment Record de show_my_pics(). Or au moment de l’exécution de la fonction par setTimeout() (après 500 ms) la boucle aura fini de tourner et i aura la valeur 4 (voir la console Firebug par exemple). Ainsi les images de 0 à 3 n’auront pas changé de propriété CSS et ne seront pas visibles.

Illustration avec notre second exemple

Suivons le même raisonnement pour notre fonction appear() qui nous permet d’atteindre le résultat que nous souhaitions (exemple 2), en nous intéressant particulièrement à l’IEF. C’est en effet la différence majeure avec l’exemple 1. A chaque exécution de l’IEF un nouveau contexte d’exécution est créé. Comme nous passons i en paramètre, l’Environment Record du Lexical Environment va enregistrer l’objet arguments avec la valeur de i. Ainsi le Lexical Environment de la fonction exécutée par setTimeout() va rechercher la valeur de key dans outer qui renvoi vers le Lexical Environment du contexte de niveau supérieur (celui de l’IEF). La valeur de key sera donc égale à i (l’argument de l’IEF), c’est ce que nous recherchons. Par exemple au troisième tour de la boucle deux objets contexte d’exécution sont successivement créés :

  1. Celui de l’IEF,
  2. celui de la fonction passée en premier argument à setTimeout().

Chacun, en partant du dernier créé, référence le précédent dans sa propriété outer. Ainsi la fonction qui modifie le style de l’élément DOM n’ayant pas de propriété key ou arguments dans son contexte va en rechercher la valeur dans le contexte extérieur. Elle va la trouver dans le contexte de l’IEF où key sera enregistré avec la valeur 2, schématiquement :

{ …

environmentRecord : { arguments : {key :2,…},…},

outer : {…}

}

Même si l’IEF a terminé son exécution, dès l’instant où une fonction imbriquée référence la variable key, cette variable reste disponible.

C’est une autre caractéristique de la façon dont ECMAScript5 gère la portée des variables. Lorsque le contexte d’exécution d’une fonction disparaît (lors du return de la fonction par exemple), dès l’instant où un autre objet "function" est défini dans ce contexte et que ce dernier référence une propriété définie dans le contexte supérieur (celui qui disparaît), ce contexte ne sera pas détruit par le garbage collector tant que la référence vers la propriété existera.
ECMAScript permet à une fonction imbriquée d’avoir toujours accès à une propriété de sa fonction parente même si cette dernière a terminé son exécution. Ces différents mécanismes permettent ainsi de conserver la valeur d’une variable, enfermée dans une fonction mais restant accessible. C’est le principe même d’une closure.

Pour aller plus loin

Les possibilités offertes par les closures en font un des mécanismes les plus puissants du langage ECMAScript5 aussi j’espère que cet article aura permis d’éclaircir si besoin leur fonctionnement et de découvrir les processus internes mis en œuvre. Les principes fondamentaux qu’il faut retenir de ce mécanisme :

Je vous invite à rechercher sur le net comment utiliser les closures, leurs applications pratiques mais aussi les réserves qui peuvent exister quant à leur utilisation comme cet article (en anglais) de Google.

Sources et liens

Vous trouverez ci-dessous une liste de liens vers des sites traitant de Javascript qui m'ont servi à la rédaction de cet article.