Certaines fonctions gardent un souvenir. Déclarée à l'intérieur d'une autre fonction, une fonction continue de voir les variables de sa voisine longtemps après que celle-ci a fini de s'exécuter, comme si elle emportait un morceau du décor avec elle.
Ce mécanisme porte un nom, la closure, et il explique une bonne part de ce qui semble mystérieux en JavaScript : les compteurs qui retiennent leur valeur, les données privées sans classe, les fonctions de rappel qui affichent un chiffre inattendu.
Définition
Les compteurs sortent de la même fabrique et pourtant leurs valeurs sont séparées de 6. Chacun garde sa propre variable total, vivante bien après la fin de creerCompteur().
Chaque ligne affiche la valeur de la variable total que ce compteur lit, et non la dernière valeur qu'il a renvoyée : c'est ce qui rend visible un compteur jamais appelé. Changer de version rejoue la même suite d'appels sur l'autre code, sans rien réinitialiser ; dans un vrai programme, il faudrait relancer la page pour changer d'implémentation. Les barres sont à l'échelle de 12 appels, puis du total dès qu'il dépasse 12, et les valeurs exactes sont écrites à côté.
Créez plusieurs compteurs et appelez-les. Chacun garde le sien, alors qu'ils viennent tous du même code. Basculez ensuite sur la variable partagée pour voir la différence.
Une closure, ou fermeture, est l'association d'une fonction et de l'environnement dans lequel elle a été déclarée. Tant que la fonction existe quelque part, les variables de cet environnement restent vivantes, même si le code qui les a créées est terminé.
function creerCompteur() {
let total = 0;
return function () {
total += 1;
return total;
};
}
const compteur = creerCompteur();
console.log(compteur()); // 1
console.log(compteur()); // 2
console.log(creerCompteur()()); // 1, un autre environnementLa variable total devrait disparaître à la fin de creerCompteur(). Elle survit parce que la fonction renvoyée la référence encore. Et chaque appel fabrique un environnement neuf : deux compteurs ne se marchent jamais dessus.
Ce que ça permet concrètement
- Un état privé.
totaln'est accessible par aucun autre code : ni lecture, ni écriture, ni suppression. - Une usine à fonctions. Une fonction qui en renvoie une autre, préréglée avec les valeurs reçues.
- Une mémoire entre deux appels. C'est exactement le principe de la Mémoïsation, qui garde un cache dans la closure.
- Un module isolé. Historiquement le rôle de l'IIFE (fonction appelée immédiatement), avant que les modules ne prennent le relais.
function multiplierPar(facteur) {
return (nombre) => nombre * facteur;
}
const doubler = multiplierPar(2);
const tripler = multiplierPar(3);
console.log(doubler(7)); // 14
console.log(tripler(7)); // 21Le piège de la boucle
C'est le bogue le plus fréquent lié aux closures, et il tient entièrement à la Portée (scope) de la variable de boucle.
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("var", i), 0);
}
// var 4, var 4, var 4
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log("let", j), 0);
}
// let 1, let 2, let 3Avec var, il n'existe qu'une seule variable pour toute la boucle : les trois fonctions la partagent et lisent sa valeur finale. Avec let, chaque tour reçoit sa propre variable, donc sa propre closure.
Ce piège est la raison la plus concrète de préférer let à var dans une boucle, bien avant les arguments de style.
Questions fréquentes
Une closure garde-t-elle toute la mémoire de la fonction parente ?
Non, les moteurs modernes ne conservent que les variables réellement référencées par la fonction interne. Le reste est libéré normalement. La fuite de mémoire arrive quand une closure survit longtemps, par exemple attachée à un écouteur jamais retiré, et retient au passage un objet volumineux.
Quelle différence avec un objet qui stockerait la même donnée ?
Un objet expose ses propriétés : n'importe quel code peut les lire et les écrire. Une closure ne laisse passer que ce que vous renvoyez explicitement. C'est la seule forme d'encapsulation réellement étanche du langage, à côté des champs privés d'une class notés avec un dièse.
Faut-il éviter les closures pour des raisons de performance ?
Non. Créer une closure coûte à peu près autant que créer un objet, ce qui est négligeable presque partout. Le seul scénario à surveiller est la création de milliers de fonctions dans une boucle serrée. Notre formation JavaScript revient sur ce mécanisme au moment des fonctions de rappel, là où il devient indispensable.