Les closures font partie des sujets que tout le monde a peur d'aborder en JavaScript. Pourtant, tu en utilises déjà sans le savoir — chaque fois qu'une fonction utilise une variable définie en dehors d'elle-même, tu crées une closure. Ce guide t'explique le mécanisme de fond, les cas d'usage pratiques, et le piège classique à éviter. Mais d'abord, un rappel sur la portée des variables — c'est là que tout commence.
La portée des variables : le socle des closures
En JavaScript, chaque variable a une portée (scope) — la zone du code où elle est accessible. Si tu as bien compris let, const et var, tu sais qu'il existe deux types de portée principaux :
// Portée globale
const message = 'Bonjour';
function direBonjour() {
// Portée locale (fonction)
const prenom = 'Alice';
console.log(message + ', ' + prenom); // Accès à message (portée parente)
}
direBonjour(); // "Bonjour, Alice"
console.log(prenom); // ❌ ReferenceError — prenom n'existe pas ici
La règle : une fonction peut accéder aux variables de son propre scope et de tous les scopes parents. Mais l'inverse n'est pas vrai — le code parent ne voit pas les variables enfants. C'est la portée lexicale.
Qu'est-ce qu'une closure exactement ?
Une closure, c'est quand une fonction se souvient des variables de son environnement de création, même après que cet environnement ait disparu. Voici le cas le plus simple :
function creerCompteur() {
let compteur = 0; // Variable locale
return function() { // Fonction retournée
compteur++;
return compteur;
};
}
const incrementer = creerCompteur();
console.log(incrementer()); // 1
console.log(incrementer()); // 2
console.log(incrementer()); // 3
Ce qui est remarquable : creerCompteur() a fini de s'exécuter. Normalement, compteur devrait être détruit. Mais la fonction retournée ferme sur (d'où closure) la variable compteur — elle en garde une référence vivante. C'est le mécanisme fondamental.
Chaque appel à creerCompteur() crée un nouveau compteur indépendant :
const compteurA = creerCompteur();
const compteurB = creerCompteur();
console.log(compteurA()); // 1
console.log(compteurA()); // 2
console.log(compteurB()); // 1 — compteurB a son propre état
console.log(compteurA()); // 3 — compteurA continue indépendamment
Les closures dans le vrai code : callbacks et event listeners
Tu utilises les closures en permanence avec les callbacks et les écouteurs d'événements. Dans la manipulation du DOM, chaque listener est une closure :
function configurerBouton(labelBouton, message) {
const bouton = document.querySelector('#' + labelBouton);
bouton.addEventListener('click', function() {
// Cette fonction ferme sur 'message'
alert(message); // message est accessible ici grâce à la closure
});
}
configureBouton('btn-aide', 'Bienvenue sur GoGoKodo !');
configureBouton('btn-contact', 'Envoie-nous un message !');
Chaque listener capture son propre message. Quand tu cliques sur chaque bouton, il affiche le bon texte — parce que chaque callback est une closure indépendante sur sa propre valeur de message.
Le piège classique : closures dans les boucles avec var
C'est l'erreur que font tous les développeurs JavaScript au moins une fois. Elle illustre parfaitement pourquoi var est problématique — et pourquoi tu devrais utiliser let.
// ❌ Avec var — tous les listeners affichent 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Affiche 3, 3, 3 — pas 0, 1, 2
}, 1000);
}
// Pourquoi ? var n'a pas de portée de bloc.
// Il n'existe qu'UN SEUL i (dans la portée de la fonction).
// Quand les callbacks s'exécutent (après 1s), la boucle est finie → i vaut 3.
// ✅ Avec let — chaque itération a son propre i
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Affiche 0, 1, 2 correctement
}, 1000);
}
// let crée une nouvelle variable i pour chaque itération.
// Chaque callback ferme sur son propre i.
Si tu dois impérativement utiliser var (code legacy), la solution historique est une IIFE :
// Solution pré-ES6 : IIFE pour créer une portée par itération
for (var i = 0; i < 3; i++) {
(function(j) { // j capture la valeur de i à ce moment
setTimeout(function() {
console.log(j); // Affiche 0, 1, 2
}, 1000);
})(i);
}
Le module pattern : encapsuler avec les closures
Les closures permettent de créer des variables privées en JavaScript — quelque chose que le langage n'avait pas nativement avant les classes ES6. C'est le module pattern :
const panier = (function() {
// Variables privées — inaccessibles depuis l'extérieur
let articles = [];
let total = 0;
return {
// Interface publique
ajouter(article, prix) {
articles.push(article);
total += prix;
},
supprimer(article) {
const idx = articles.indexOf(article);
if (idx > -1) articles.splice(idx, 1);
},
getTotal() {
return total;
},
afficher() {
return `${articles.length} article(s) — ${total}€`;
}
};
})();
panier.ajouter('Livre JS', 29);
panier.ajouter('Clavier', 89);
console.log(panier.afficher()); // "2 article(s) — 118€"
console.log(panier.articles); // undefined — privé !
Le code externe peut appeler les méthodes publiques, mais il ne peut pas accéder directement à articles ou total. Les closures créent un état encapsulé — c'est le fondement de nombreux patterns JavaScript.
Closures et fonctions d'ordre supérieur
Les closures sont au cœur des fonctions d'ordre supérieur comme map, filter, reduce. Voici un exemple concret :
// Créer un multiplicateur configurable
function creerMultiplicateur(facteur) {
return (nombre) => nombre * facteur; // ferme sur 'facteur'
}
const doubler = creerMultiplicateur(2);
const tripler = creerMultiplicateur(3);
const parDix = creerMultiplicateur(10);
[1, 2, 3, 4, 5].map(doubler); // [2, 4, 6, 8, 10]
[1, 2, 3, 4, 5].map(tripler); // [3, 6, 9, 12, 15]
[1, 2, 3, 4, 5].map(parDix); // [10, 20, 30, 40, 50]
Chaque fonction créée ferme sur sa propre valeur de facteur. C'est élégant, réutilisable, et très lisible.
Closures et mémorisation (memoization)
Un usage avancé : mettre en cache les résultats de calculs coûteux.
function memoriser(fn) {
const cache = {}; // Fermé dans la closure
return function(n) {
if (cache[n] !== undefined) {
console.log('Cache hit pour', n);
return cache[n];
}
cache[n] = fn(n);
return cache[n];
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fibMemo = memoriser(fibonacci);
fibMemo(40); // Calcule (lent)
fibMemo(40); // Retourne instantanément depuis le cache
Ce qu'il faut retenir
Les closures ne sont pas une feature obscure de JavaScript — elles sont le comportement naturel des fonctions dans un langage à portée lexicale. Voici les points clés :
- Une closure = une fonction qui se souvient de son environnement de création
- Chaque appel de fonction crée un nouveau scope, donc une nouvelle closure
- Utilise
letdans les boucles pour éviter le piège classique - Le module pattern exploite les closures pour l'encapsulation
- Les fonctions d'ordre supérieur (
map,filter) sont bâties sur ce principe
La prochaine fois que tu vois une fonction imbriquée dans une autre, demande-toi : quelles variables de l'environnement parent est-ce qu'elle capture ? La réponse, c'est ta closure.
Pour pratiquer les closures et toute la syntaxe JavaScript moderne directement dans le navigateur, retrouve nos ateliers interactifs GoGoKodo. Et pour aller plus loin sur les fonctions asynchrones qui exploitent ces mêmes mécanismes, notre guide sur async/await et les Promises est une suite naturelle.
Pour manipuler tes tableaux de façon moderne et sans boucles, découvre notre guide complet sur map(), filter() et reduce() — les méthodes qui font passer ton JavaScript au niveau supérieur.