Les closures en JavaScript : comprendre et maîtriser
Mark Toledo
1 avril 2026

Les closures sont le sujet qui revient le plus souvent dans les entretiens techniques JavaScript. Et pourtant, c'est aussi le concept que beaucoup de développeurs utilisent quotidiennement sans vraiment comprendre ce qui se passe sous le capot. Je me souviens de mon premier entretien sérieux pour un poste chez une startup parisienne — la question sur les closures m'a pris de court parce que j'utilisais des closures sans le savoir, mais je n'aurais pas pu les expliquer.
Ce guide, c'est ce que j'aurais voulu lire à l'époque.
La portée lexicale : le fondement des closures
Pour comprendre les closures, il faut d'abord comprendre la portée lexicale. En JavaScript, quand tu écris une fonction, cette fonction a accès aux variables de l'environnement où elle a été définie — pas où elle est appelée.
const message = 'Bonjour';
function saluer() {
console.log(message); // Accède à la variable du scope parent
}
function executerDansAutreContexte() {
const message = 'Au revoir';
saluer(); // Affiche "Bonjour", pas "Au revoir"
}
executerDansAutreContexte();
saluer est définie dans le scope global où message vaut 'Bonjour'. Peu importe depuis où on l'appelle, elle voit toujours son environnement de définition.
Définition d'une closure
Une closure, c'est une fonction qui "se souvient" de l'environnement dans lequel elle a été créée, même quand cet environnement n'existe plus.
function creerCompteur() {
let compte = 0; // Variable locale à creerCompteur
return function() {
compte++;
return compte;
};
}
const compteur = creerCompteur();
console.log(compteur()); // 1
console.log(compteur()); // 2
console.log(compteur()); // 3
Quand creerCompteur se termine, on pourrait penser que compte disparaît de la mémoire. Mais non — la fonction retournée maintient une référence vers son scope parent. La variable compte reste en vie tant que la closure existe.
Chaque appel à creerCompteur crée un nouvel environnement indépendant :
const compteurA = creerCompteur();
const compteurB = creerCompteur();
compteurA(); // 1
compteurA(); // 2
compteurB(); // 1 — compteurB a son propre `compte`
Cas d'usage concrets
État privé
Avant les classes ES6, les closures étaient le seul moyen de créer des données privées en JavaScript. Même avec les classes, ce pattern reste utile.
function creerBanque(soldeInitial) {
let solde = soldeInitial; // Inaccessible de l'extérieur
return {
deposer(montant) {
if (montant <= 0) throw new Error('Montant invalide');
solde += montant;
return solde;
},
retirer(montant) {
if (montant > solde) throw new Error('Solde insuffisant');
solde -= montant;
return solde;
},
getSolde() {
return solde;
}
};
}
const compte = creerBanque(1000);
compte.deposer(500); // 1500
compte.retirer(200); // 1300
console.log(compte.solde); // undefined — `solde` est privé
Fonctions partiellement appliquées (currying)
Une closure permet de créer des fonctions spécialisées à partir d'une fonction générique.
function multiplierPar(facteur) {
return (nombre) => nombre * facteur;
}
const doubler = multiplierPar(2);
const tripler = multiplierPar(3);
console.log(doubler(5)); // 10
console.log(tripler(5)); // 15
// Très pratique avec les méthodes de tableau
const nombres = [1, 2, 3, 4, 5];
console.log(nombres.map(doubler)); // [2, 4, 6, 8, 10]
Mémoïzation
Une closure peut aussi servir à cacher les résultats d'une fonction coûteuse. J'utilise ce pattern pour des calculs répétitifs dans des dashboards analytics.
function memoiser(fn) {
const cache = new Map();
return function(...args) {
const cle = JSON.stringify(args);
if (cache.has(cle)) {
console.log('Cache hit');
return cache.get(cle);
}
const resultat = fn.apply(this, args);
cache.set(cle, resultat);
return resultat;
};
}
const calculerFibonacci = memoiser(function fib(n) {
if (n <= 1) return n;
return calculerFibonacci(n - 1) + calculerFibonacci(n - 2);
});
calculerFibonacci(40); // Calcule une fois
calculerFibonacci(40); // Cache hit — instantané
Le piège classique avec les boucles
C'est le bug le plus fréquent lié aux closures, et j'ai vu ce problème dans du code de développeurs expérimentés.
// ❌ Bug — toutes les fonctions affichent 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Affiche 3, 3, 3
}, 100);
}
Le problème : toutes les fonctions de callback partagent la même référence à i. Quand elles s'exécutent (après la boucle), i vaut 3.
Trois façons de corriger :
// ✅ Solution 1 : let (scope de bloc)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// ✅ Solution 2 : IIFE (closure immédiate)
for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 100))(i);
}
// ✅ Solution 3 : bind
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 100);
}
La solution let est de loin la plus lisible — chaque itération de boucle crée un nouveau binding de i.
Les closures et les event listeners
Un piège courant dans le code front-end :
function attacherListeners() {
const boutons = document.querySelectorAll('.btn');
// ❌ Tous les listeners partagent la même référence à `i`
for (var i = 0; i < boutons.length; i++) {
boutons[i].addEventListener('click', function() {
console.log('Bouton ' + i + ' cliqué'); // Toujours le dernier index
});
}
// ✅ Avec let
for (let i = 0; i < boutons.length; i++) {
boutons[i].addEventListener('click', function() {
console.log('Bouton ' + i + ' cliqué'); // Index correct
});
}
}
Le pattern module
Avant ES6 modules, les closures étaient utilisées pour créer des modules avec des API publiques et des données privées. Ce pattern s'appelle IIFE Module Pattern.
const MonModule = (function() {
// Données privées
const config = { debug: false };
let compteurRequetes = 0;
// Méthodes privées
function logger(msg) {
if (config.debug) console.log(`[Module] ${msg}`);
}
// API publique
return {
activer() {
config.debug = true;
},
async requete(url) {
compteurRequetes++;
logger(`Requête #${compteurRequetes} vers ${url}`);
return fetch(url).then(r => r.json());
},
getStats() {
return { requetes: compteurRequetes };
}
};
})();
MonModule.activer();
MonModule.requete('/api/donnees');
Avec ES6 modules natifs, on n'a plus besoin de ce pattern — les imports/exports gèrent la visibilité. Mais comprendre l'IIFE pattern aide à lire du code legacy.
Fuites mémoire avec les closures
Les closures peuvent causer des fuites mémoire si elles maintiennent des références à de grands objets inutilement.
// ❌ Potentielle fuite — donneesMassives reste en mémoire
function creerClosure() {
const donneesMassives = new Array(1000000).fill('donnee');
return function() {
console.log('Je retiens tout le tableau en mémoire');
};
}
// ✅ Libérer ce qu'on n'utilise pas
function creerClosurePropre() {
const donneesMassives = new Array(1000000).fill('donnee');
const longueur = donneesMassives.length; // Extraire seulement ce dont on a besoin
return function() {
console.log(`Tableau de ${longueur} éléments`);
// donneesMassives peut maintenant être collecté par le GC
};
}
Ce qu'on retient
Une closure, c'est simplement une fonction combinée avec son environnement lexical. C'est un comportement naturel de JavaScript, pas une fonctionnalité spéciale.
Les cas d'usage concrets : état privé, fonctions partielles, mémoïzation, event handlers dans des boucles. Le piège le plus courant : oublier que var n'a pas de portée de bloc dans les boucles — let et const ont résolu ce problème pour les nouveaux projets.
Comprendre les closures, c'est comprendre comment JavaScript gère la mémoire et la portée. Ça aide à écrire du code plus clair, à débugger des comportements inattendus, et à lire du code que tu n'as pas écrit.
