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 !
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, carmyVar
est 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
àmyVar
estarea()
, 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
àmyVar
car 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 puisquemyVar
est 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();
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
variablevar1
et que vous la passez en référence
àvar2
,
alorsvar1
etvar2
pointeront
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
variablemyVar
dans la fonctionshow()
malgré
la fin de l'exécution dearea()
.
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.
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++;
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
denumber
de 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 !
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
}
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 variablei
vaut 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 !
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 variablei
change 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 variablecurrentI
est 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 variablecurrentI
est
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);
})();
}
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
variablecurrentI
est 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) variablecurrentI
qu'elle connaît, celle déclarée dans
notre IIFE, car elle n'a pas accès aux autres
variablescurrentI
dé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é
:currentI
est 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 argumentcurrentI
pour
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);
}
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
variablei
diffé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.
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 variablemyStatic
est 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 àmyVar
par 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éstatic
existe 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éstatic
en 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éreturn
et
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
Et voilà une fonction avec une
variable statique nomméemyVar
! Cela pourra vous être utile par
moments (bien que cela soit assez rare).
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.