Closure en JavaScript : une fonction qui se souvient de son environnement

Une closure est une fonction qui garde accès aux variables de l'endroit où elle a été créée, même une fois ce code terminé. Explication et pièges.
4 min de lecture
Believemy logo

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

Déplacez une seule ligne, et les compteurs cessent d'être indépendants
function creerCompteur() {
let total = 0;
return () => (total += 1);
}
compteurA
7 appels
7
compteurB
1 appel
1
8 appels au total, écart entre compteurs : 6

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é.

JAVASCRIPT
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 environnement

La 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é. total n'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.
JAVASCRIPT
function multiplierPar(facteur) {
  return (nombre) => nombre * facteur;
}

const doubler = multiplierPar(2);
const tripler = multiplierPar(3);

console.log(doubler(7)); // 14
console.log(tripler(7)); // 21


Le 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.

JAVASCRIPT
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 3

Avec 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.

Bon à savoir

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

Question

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.


Question

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.


Question

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.

Termes connexes

Découvrez notre glossaire JavaScript

Tous les mots de JavaScript expliqués simplement : mots-clés, objets natifs, méthodes, erreurs et concepts. Définitions claires et exemples qui tournent, pour apprendre et pour se dépanner.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.