Les closures : une fonction qui se souvient de son environnement
Comprenez pourquoi une fonction garde accès aux variables de l’endroit où elle a été créée, et ce que ce mécanisme permet : compteurs, données privées et callbacks.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
Pourquoi une fonction continue d’accéder à des variables créées ailleurs, longtemps après que le code qui les a créées est terminé.
Une fonction emporte son environnement
function creerCompteur() {
let compteur = 0;
return function () {
compteur += 1;
return compteur;
};
}
const suivant = creerCompteur();
console.log(suivant()); // 1
console.log(suivant()); // 2
creerCompteur() a terminé son exécution, pourtant compteur existe toujours. La fonction retournée en a besoin : le moteur conserve donc l’environnement dont elle dépend. C’est une closure.
Deux appels, deux mémoires
const a = creerCompteur();
const b = creerCompteur();
console.log(a(), a(), b()); // 1 2 1
a └── compteur = 2
b └── compteur = 1
Chaque appel à la fabrique crée un environnement neuf. Même code, mémoires indépendantes.
Une donnée réellement privée
Si la fabrique renvoie un objet de fonctions plutôt qu’une fonction seule, la variable interne devient une donnée privée. Un compte.solde lu de l’extérieur vaut undefined : seules les fonctions créées à l’intérieur de la fabrique savent y accéder. Vous obtenez une forme simple d’encapsulation, sans classe ni mot-clé particulier.
Vous en écrivez déjà
Chaque callback est concerné. Dans setTimeout(() => console.log(user.name), 1000), la fonction s’exécute une seconde plus tard, alors que la fonction appelante est terminée depuis longtemps ; user reste pourtant accessible. Même chose pour un gestionnaire de clic React qui lit une variable déclarée dans le composant.
Le revers : tant que la fonction retournée existe, ce dont elle dépend reste généralement en mémoire. Garder une référence vers un très gros objet dans une closure conservée longtemps, par exemple un écouteur d’événement jamais retiré, peut retenir cette mémoire plus longtemps que prévu.
Fiche mémo
- Une closure, c’est une fonction plus les variables de l’endroit où elle a été créée, et non de l’endroit où elle est appelée.
- Une fabrique appelée deux fois produit deux environnements distincts.
- Les variables capturées ne sont pas copiées : elles restent partagées et modifiables.
- Usages courants : compteurs, état interne, données privées, callbacks, fonctions préconfigurées.
- Limite : une closure vivante retient ce qu’elle référence.
L’idée à emporter : une fonction ne transporte pas seulement du code, elle transporte l’accès aux variables dont elle a besoin.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinLe piège de la boucle : pourquoi var et let ne capturent pas la même choseDévelopper le coursReplier le cours
Ce que vous saurez faire
Prévoir ce qu’une closure capture réellement, reconnaître le piège de boucle qui affiche trois fois la même valeur, et construire une donnée privée dont vous connaissez les limites. Tous les exemples s’exécutent tels quels dans la console d’un navigateur récent ou dans Node.js 18 ou plus.
1. Une closure capture la variable, pas sa valeur
C’est le point que l’on comprend souvent de travers. La fonction ne garde pas une photographie de l’état au moment de sa création : elle garde un accès à la variable elle-même.
let message = 'bonjour';
const dire = () => console.log(message);
message = 'bonsoir';
dire(); // "bonsoir"
Résultat attendu : bonsoir. La closure lit la variable au moment de l’appel. Ce comportement est une force, puisqu’un compteur peut s’incrémenter, et une source de surprise, comme la section suivante le montre.
Référence : les fermetures.
2. Le piège de la boucle : var contre let
Voici le cas que rencontrent réellement les lecteurs, et qu’il vaut mieux avoir vu une fois.
for (var i = 0; i < 3; i += 1) {
setTimeout(() => console.log('var', i), 0);
}
for (let j = 0; j < 3; j += 1) {
setTimeout(() => console.log('let', j), 0);
}
Résultat attendu dans la console :
var 3
var 3
var 3
let 0
let 1
let 2
L’explication tient en une phrase : var crée une seule variable pour toute la fonction, tandis que let en crée une par tour de boucle.
Avec var, les trois fonctions différées partagent la même variable i. Elles ne s’exécutent qu’après la fin de la boucle, moment où i vaut déjà 3, la valeur qui a fait sortir de la boucle. Elles lisent donc toutes 3.
Avec let, chaque tour dispose de sa propre variable j, initialisée avec la valeur du tour précédent puis incrémentée. Chaque fonction différée capture une variable différente et conserve donc sa valeur.
var → i unique ← fonction 1, fonction 2, fonction 3
let → j (tour 0) ← fonction 1
j (tour 1) ← fonction 2
j (tour 2) ← fonction 3
Ce piège ne dépend pas de setTimeout : il apparaît partout où la fonction est appelée plus tard, dans un écouteur d’événement, une promesse ou un callback.
Référence : déclaration let.
3. Une donnée privée, et ce qu’elle vaut vraiment
function creerCompte() {
let solde = 0;
return {
deposer(montant) {
if (!Number.isFinite(montant) || montant <= 0) return false;
solde += montant;
return true;
},
lireSolde() {
return solde;
},
};
}
const compte = creerCompte();
compte.deposer(100);
compte.deposer(50);
console.log(compte.lireSolde()); // 150
console.log(compte.solde); // undefined
solde n’est pas une propriété de l’objet renvoyé : rien à l’extérieur ne peut la lire ni la modifier directement. C’est de l’encapsulation, pas de la sécurité. Un autre script exécuté dans la même page peut toujours remplacer compte.deposer par sa propre fonction. Les champs privés de classe, notés avec un croisillon, offrent aujourd’hui une alternative plus explicite pour le même besoin.
Pièges et limites
- La capture est partagée. Deux fonctions créées dans le même environnement modifient la même variable. Ce n’est pas un bug, mais cela surprend quand on croyait avoir deux compteurs indépendants.
- La rétention mémoire. Un écouteur d’événement jamais retiré garde vivant tout ce que sa closure référence, y compris un gros objet devenu inutile. Retirez l’écouteur quand le composant disparaît.
- La closure périmée. Dans une interface React, une fonction enregistrée une seule fois continue de lire les valeurs du premier rendu. Le langage se comporte exactement comme prévu ; c’est l’attente de l’interface qui était fausse.
- Le coût en nombre d’objets. Chaque appel de fabrique crée de nouvelles fonctions. C’est négligeable dans la plupart des cas, mais mesurable si vous en créez des milliers dans une boucle serrée.
À vous de jouer
Ce code devrait afficher bouton A, bouton B, bouton C. Il affiche trois fois la même chose.
const boutons = ['A', 'B', 'C'];
for (var index = 0; index < boutons.length; index += 1) {
setTimeout(() => console.log('bouton', boutons[index]), 10);
}
Corrigez-le de deux façons différentes, sans modifier l’appel à setTimeout. Dites ensuite laquelle vous garderiez.
Correction commentée
Le code affiche trois fois bouton undefined : au déclenchement des minuteries, index vaut 3, et boutons[3] n’existe pas.
Première correction, la plus directe : remplacer var par let.
for (let index = 0; index < boutons.length; index += 1) {
setTimeout(() => console.log('bouton', boutons[index]), 10);
}
// bouton A, bouton B, bouton C
Chaque tour crée sa propre variable index, et chaque fonction différée capture la sienne.
Seconde correction : donner à chaque fonction son propre paramètre.
boutons.forEach((nom) => {
setTimeout(() => console.log('bouton', nom), 10);
});
// bouton A, bouton B, bouton C
Ici, forEach appelle la fonction une fois par élément. Chaque appel crée son propre paramètre nom, donc son propre environnement : le problème disparaît par construction. La fonction immédiatement invoquée, qui consistait à envelopper le corps de la boucle pour figer la valeur, produit le même effet ; c’était le contournement historique avant let, plus verbeux à lire aujourd’hui.
Laquelle garder ? forEach quand vous parcourez une collection, car l’intention est explicite. let quand vous avez besoin d’un vrai compteur numérique. Dans les deux cas, le principe est identique : pour que trois fonctions se souviennent de trois valeurs, il faut trois variables, donc trois environnements.
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.