Les closures

Au cours de la lecture de ce tutoriel, vous avez très probablement dû constater que les fonctions anonymes étaient très fréquemment utilisées pour diverses choses, comme les événements, les isolements de code, etc. Leurs utilisations sont nombreuses et variées, car elles sont très facilement adaptables à toutes les situations. Et s'il y a bien un domaine où les fonctions anonymes excellent, c'est bien les closures !

Les variables et leurs accès

Avant d'attaquer l'étude des closures, il est de bon ton d'étudier un peu plus en profondeur de quelle manière sont gérées les variables par le JavaScript.

Commençons par ce code :

function area() {
var myVar = 1;
}
area(); // On exécute la fonction, ce qui crée la variable « myVar »
alert(myVar);

Même sans l'exécuter, vous vous doutez sûrement du résultat que nous allons obtenir : une erreur. Ceci est normal, carmyVarest déclarée dans une fonction tandis que nous essayons d'y accéder depuis l'espace global (en cas d'oubli, nous vous invitons à relire cette sous-partie ).

La seule fonction capable d'accéder àmyVarestarea(), car c'est elle qui l'a créée. Seulement, une fois l'exécution de la fonction terminée, la variable est supprimée et devient donc inaccessible.

Maintenant, si nous faisons ceci :

function area() {
var myVar = 1;
function show() {
alert(myVar);
}
}
area();
alert(myVar);

Le résultat est toujours le même, il est nul. Cependant, en plus de la fonctionarea(), la fonctionshow()est maintenant capable, elle aussi, d'accéder àmyVarcar elle a été créée dans le même espace que celui demyVar. Mais pour cela il faudrait l'exécuter.

Plutôt que de l'exécuter immédiatement, nous allons l'exécuter une seconde après l'exécution de notre fonctionarea(), ce qui devrait normalement retourner une erreur puisquemyVarest censée être détruite une fois qu'area()a terminé son exécution.

function area() {
var myVar = 1;
function show() {
alert(myVar);
}
setTimeout(show, 1000);
}
area();

Essayer le code

Et, par miracle, cela fonctionne ! Vous n'êtes probablement pas surpris, cela fait déjà plusieurs fois que vous savez qu'il est possible d'accéder à une variable même après la disparition de l'espace dans lequel elle a été créée (ici, la fonctionarea()). Cependant, savez-vous pourquoi ?

Vous souvenez-vous de la formulation « passer une variable par référence » ? Cela signifie que vous permettez que la variable soit accessible par un autre nom que celui d'origine. Ainsi, si vous avez une variablevar1et que vous la passez en référence àvar2, alorsvar1etvar2pointeront sur la même variable. Donc, en modifiantvar1, cela affecteravar2, et vice versa.

Tout cela nous amène à la constatation suivante : une variable peut posséder plusieurs références. Dans notre fonctionarea(), nous avons une première référence vers notre variable, car elle y est déclarée sous le nommyVar. Dans la fonctionshow(), nous avons une deuxième référence du même nom,myVar.

Quand une fonction termine son exécution, la référence vers la variable est détruite, rendant son accès impossible. C’est ce qui se produit avec notre fonctionarea(). La variable en elle-même continue à exister tant qu'il reste encore une référence qui est susceptible d'être utilisée. C'est aussi ce qui se produit avec la fonctionshow(). Puisque celle-ci possède une référence vers notre variable, cette dernière n'est pas détruite.

Ainsi, une variable peut très bien perdre dix de ses références, elle ne sera pas supprimée tant qu'il lui en restera au moins une. C'est ce qui explique que nous puissions accéder à la variablemyVardans la fonctionshow()malgré la fin de l'exécution dearea().

Comprendre le problème

Les closures n'existent pas simplement pour décorer, il existe des raisons bien particulières pour lesquelles elles ont été conçues. Les problèmes qu'elles sont supposées résoudre ne sont pas simples à comprendre, nous allons tâcher de vous expliquer cela au mieux.

Premier exemple

Commençons par un exemple simple qui vous donnera un aperçu de l'ampleur du problème :

var number = 1;
setTimeout(function() {
alert(number);
}, 100);
number++;

Essayer le code

Si vous avez essayé le code, alors vous avez sûrement remarqué le problème : la fonctionalert()ne nous affiche pas la valeur 1 comme nous pourrions le penser, mais la valeur 2. Nous avons pourtant fait appel àsetTimeout()avant le changement de valeur, alors comment se fait-il qu'il y ait ce problème ?

Eh bien, cela vient du fait que ce n'est que la fonctionsetTimeout()qui a été exécutée avant le changement de valeur. La fonction anonyme, elle, n'est exécutée que 100 millisecondes après l'exécution desetTimeout(), ce qui a largement laissé le temps à la valeur denumberde changer.

Si cela vous semble étrange, c’est probablement parce que vous partez du principe que, lorsque nous déclarons notre fonction anonyme, celle-ci va directement récupérer les valeurs des variables utilisées. Que nenni ! Lorsque vous déclarez votre fonction en écrivant le nom d'une variable, vous passez une référence vers cette variable à votre fonction. Cette référence sera ensuite utilisée pour connaître la valeur de la variable, mais seulement une fois la fonction exécutée !

Maintenant que le problème est probablement plus clair dans votre tête, passons à un exemple plus concret !

Un cas concret

Admettons que vous souhaitiez faire apparaître une dizaine de balises<div>de manière progressive, les unes à la suite des autres. Voici le code que vous tenteriez probablement de faire dans l'état actuel de vos connaissances :

var divs = document.getElementsByTagName('div'),
divsLen = divs.length;
for (var i = 0; i < divsLen; i++) {
setTimeout(function() {
divs[i].style.display = 'block';
}, 200 * i); // Le temps augmentera de 200 ms à chaque élément
}

Essayer le code

Alors ? Le résultat n'est pas très concluant, n'est-ce pas ? Si vous jetez un coup d’œil à la console d'erreurs, vous constaterez qu'elle vous signale que la variabledivs[i]est indéfinie, et ce dix fois de suite, ce qui correspond à nos dix itérations de boucle. Si nous regardons d'un peu plus près le problème, nous constatons alors que la variableivaut toujours 10 à chaque fois qu'elle est utilisée dans les fonctions anonymes, ce qui correspond à sa valeur finale une fois que la boucle a terminé son exécution.

Ceci nous ramène au même problème : notre fonction anonyme ne prend en compte que la valeur finale de notre variable. Heureusement, il existe les closures, qui peuvent contourner ce désagrément !

Explorer les solutions

Tout d'abord, qu'est-ce qu'une closure ? En JavaScript, il s'agit d'une fonction ayant pour but de capter des données susceptibles de changer au cours du temps, de les enregistrer dans son espace fonctionnel et de les fournir en cas de besoin.

Reprenons notre deuxième exemple et voyons comment lui créer une closure pour la variablei. Voici le code d'origine :

var divs = document.getElementsByTagName('div'),
divsLen = divs.length;
for (var i = 0; i < divsLen; i++) {
setTimeout(function() {
divs[i].style.display = 'block';
}, 200 * i);
}

Actuellement, le problème se situe dans le fait que la variableichange de valeur avant même que nous n'ayons eu le temps d'agir. Le seul moyen serait donc d'enregistrer cette valeur quelque part. Essayons :

var divs = document.getElementsByTagName('div'),
divsLen = divs.length;
for (var i = 0; i < divsLen; i++) {
var currentI = i; // Déclarer une variable DANS une boucle n'est pas conseillé, ici c'est juste pour l'exemple
setTimeout(function() {
divs[currentI].style.display = 'block';
}, 200 * i);
}

Malheureusement, cela ne fonctionne pas, car nous en revenons toujours au même : la variablecurrentIest réécrite à chaque tour de boucle, car le JavaScript ne crée pas d'espace fonctionnel spécifique pour une boucle. Toute variable déclarée au sein d'une boucle est déclarée dans l'espace fonctionnel parent à la boucle. Cela nous empêche donc de converser avec la valeur écrite dans notre variable, car la variable est réécrite à chaque itération de la boucle.

Cependant, il est possible de contourner cette réécriture.

Actuellement, notre variablecurrentIest déclarée dans l'espace global de notre code. Que se passerait-il si nous la déclarions à l'intérieur d'une IIFE ? Eh bien, la variable serait déclarée dans l'espace de la fonction, rendant impossible sa réécriture depuis l'extérieur.

Oui, mais si l'accès à cette variable est impossible depuis l'extérieur, comment peut-on alors l'utiliser pour notresetTimeout()?

La réponse est simple : en utilisant lesetTimeout()dans la fonction contenant la variable ! Essayons :

var divs = document.getElementsByTagName('div'),
divsLen = divs.length;
for (var i = 0; i < divsLen; i++) {
(function() {
var currentI = i;
setTimeout(function() {
divs[currentI].style.display = 'block';
}, 200 * i);
})();
}

Essayer le code

Pratique, non ? Le fonctionnement peut paraître un peu absurde la première fois que l'on découvre ce concept, mais au final il est parfaitement logique.

Étudions le principe actuel de notre code : à chaque tour de boucle, une IIFE est créée. À l'intérieur de cette dernière, une variablecurrentIest déclarée, puis nous lançons l'exécution différée d'une fonction anonyme faisant appel à cette même variable. Cette dernière fonction va utiliser la première (et la seule) variablecurrentIqu'elle connaît, celle déclarée dans notre IIFE, car elle n'a pas accès aux autres variablescurrentIdéclarées dans d'autres IIFE.

Vous n'avez toujours pas oublié la première sous-partie de ce chapitre, n'est-ce pas ? Car, si nous avons traité le sujet des variables, c'est pour vous éviter une mauvaise compréhension à ce stade du chapitre. Ici nous avons un cas parfait de ce que nous avons étudié :currentIest déclarée dans une IIFE, sa référence est donc détruite à la fin de l'exécution de l'IIFE. Cependant, nous y avons toujours accès dans notre fonction anonyme exécutée en différé, car nous possédons une référence vers cette variable, ce qui évite sa suppression.

Dernière chose, vous risquerez de tomber assez fréquemment sur des closures plutôt écrites de cette manière :

var divs = document.getElementsByTagName('div'),
divsLen = divs.length;
for (var i = 0; i < divsLen; i++) {
(function(currentI) {
setTimeout(function() {
divs[currentI].style.display = 'block';
}, 200 * i);
})(i);
}

Concrètement, qu'est-ce que l'on a fait ? Eh bien, nous avons tout simplement créé un argumentcurrentIpour notre IIFE et nous lui passons en paramètre la valeur dei. Cette modification fait gagner un peu d'espace (suppression de la ligne 8) et permet de mieux organiser le code, on distingue plus facilement ce qui constitue la closure ou non.

Tant que nous y sommes, nous pouvons nous permettre d'apporter une modification de plus à la ligne 6 :

var divs = document.getElementsByTagName('div'),
divsLen = divs.length;
for (var i = 0; i < divsLen; i++) {
(function(i) {
setTimeout(function() {
divs[i].style.display = 'block';
}, 200 * i);
})(i);
}

Essayer le code final

Ainsi, même dans la closure, nous utilisons une variable nomméei. Cela est bien plus pratique à gérer et prête moins à confusion pour peu que l'on ait compris que dans la closure nous utilisons une variableidifférente de celle située en-dehors de la closure.

Voilà, vous savez maintenant vous servir des closures dans leur cadre général. Bien qu'elles existent sous plusieurs formes et pour plusieurs cas d'utilisation, nous avons ici étudié le cas principal.

Une autre utilité, les variables statiques

Nous venons de voir un cas d'utilisation des closures. Cependant, leur utilisation ne se limite pas uniquement à ce cas de figure, elles permettent de résoudre de nombreux casse-têtes en JavaScript. Un cas provoquant assez souvent quelques prises de tête dans ce langage est l’inexistence d'un système natif de variables statiques.

Si vous avez déjà codé avec quelques autres langages, vous avez probablement déjà étudié les variables statiques. En C, elles se présentent sous cette forme :

void myFunction() {
static int myStatic = 0;
}

Ces variables particulières sont déclarées à la première exécution de la fonction, mais ne sont pas supprimées à la fin des exécutions. Elles sont conservées pour les prochaines utilisations de la fonction.

Ainsi, dans ce code en C, la variablemyStaticest déclarée et initialisée à 0 lors de la première exécution demyFunction(). La prochaine exécution de la fonction ne déclarera pas de nouveau cette variable, mais la réutilisera avec la dernière valeur qui lui a été affectée.

En gros, c'est comme si vous déclariez une variable globale en JavaScript et que vous l'utilisiez dans votre fonction : la variable et sa valeur ne seront jamais détruites. En revanche, la variable globale est accessible par toutes les fonctions, tandis qu'une variable statique n'est accessible que pour la fonction qui a fait sa déclaration.

En JavaScript, nous pouvons faire ceci :

var myVar = 0;
function display(value) {
if (typeof value != 'undefined') {
myVar = value;
}
alert(myVar);
}
display(); // Affiche : 0
display(42); // Affiche : 42
display(); // Affiche : 42

Alors que nous voudrions arriver à ceci afin d'éviter l'accès àmyVarpar une fonction autre quedisplay():

function display(value) {
static var myVar = 0;
if (typeof value != 'undefined') {
myVar = value;
}
alert(myVar);
}
display(); // Affiche : 0
display(42); // Affiche : 42
display(); // Affiche : 42

Je viens de voir que le mot-cléstaticexiste en JavaScript, pourquoi ne pas l'utiliser ?

Ah oui ! Il s'agit d'une petite incohérence (de plus) en JavaScript. Il faut savoir que ce langage a réservé de nombreux mots-clés alors qu'ils lui sont inutiles. Le mot-cléstaticen fait partie. Autrement dit, il est réservé, mais ne sert à rien et n'a donc aucune influence sur votre code (mis à part le fait de générer une erreur).

La solution se trouve donc avec les closures. En respectant le schéma classique d'une closure, une IIFE avec une fonction anonyme à l'intérieur, nous pouvons déclarer une variable dans l'IIFE et ainsi elle ne sera utilisable que par la fonction anonyme et ne sera jamais supprimée :

(function() {
var myVar = 0;
function() {
// Du code…
}
})();

Cependant, comment accéder à notre fonction anonyme ? La solution est simple : en la retournant avec le mot-cléreturnet en passant sa référence à une variable :

var myFunction = (function() {
var myVar = 0;
return function() {
// Du code…
};
})();

Si nous reprenons notre exemple, mais adapté de manière à ce qu'il possède une variable statique, alors nous obtenons ceci :

var display = (function() {
var myVar = 0; // Déclaration de la variable pseudo-statique
return function(value) {
if (typeof value != 'undefined') {
myVar = value;
}
alert(myVar);
};
})();
display(); // Affiche : 0
display(42); // Affiche : 42
display(); // Affiche : 42

Essayer le code

Et voilà une fonction avec une variable statique nomméemyVar! Cela pourra vous être utile par moments (bien que cela soit assez rare).

En résumé
  • Une variable peut posséder plusieurs références. Elle ne sera jamais supprimée tant qu'elle possèdera encore une référence active.

  • Les closures ont été inventées dans le but de répondre à plusieurs problématiques concernant la gestion de données.

  • Une closure peut être écrite de plusieurs manières différentes, à vous de choisir celle qui convient le mieux à votre code.