Closures et portée en JavaScript expliqués simplement
Vous écrivez une fonction JavaScript qui en retourne une autre. La fonction interne utilise une variable de la fonction externe, mais lorsque vous l'appelez plus tard, la fonction externe a déjà terminé. Pourtant, la variable est toujours là. Comment ? C'est la magie des closures et de la portée. De nombreux développeurs ont du mal avec ce concept, ce qui entraîne des bugs comme des valeurs de variables inattendues ou des fuites de mémoire. Dans cet article, nous allons décomposer les closures et la portée en JavaScript en termes simples avec des exemples pratiques.
Qu'est-ce que la portée en JavaScript ?
La portée détermine où les variables sont accessibles dans votre code. JavaScript a trois principaux types de portée :
- Portée globale : Les variables déclarées en dehors de toute fonction ou bloc sont accessibles globalement.
- Portée de fonction : Les variables déclarées à l'intérieur d'une fonction ne sont accessibles qu'au sein de cette fonction.
- Portée de bloc : Les variables déclarées avec
letetconstà l'intérieur d'un bloc{}ne sont accessibles qu'au sein de ce bloc.
Voici un exemple rapide :
let globalVar = 'I am global';
function myFunc() {
let functionVar = 'I am in function scope';
if (true) {
let blockVar = 'I am in block scope';
console.log(globalVar); // accessible
console.log(functionVar); // accessible
console.log(blockVar); // accessible
}
console.log(blockVar); // ReferenceError: blockVar is not defined
}
myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined
Comprendre la portée est crucial car cela évite les collisions de variables et aide à gérer la mémoire.
Lexical Scoping : La base des closures
JavaScript utilise le lexical scoping, ce qui signifie que la portée d'une variable est déterminée par sa position dans le code source. Les fonctions internes ont accès aux variables de leur portée externe (englobante). C'est aussi appelé static scoping.
Considérez ceci :
function outer() {
let outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer'
}
inner();
}
outer();
La fonction inner peut accéder à outerVar car elle est lexicalement à l'intérieur de outer. Ce n'est pas dynamique ; cela ne dépend pas de la façon dont la fonction est appelée.
Qu'est-ce qu'une closure ?
Une closure est une fonction qui se souvient de sa portée lexicale même lorsqu'elle est exécutée en dehors de cette portée. En d'autres termes, une closure vous donne accès à la portée d'une fonction externe depuis une fonction interne. Les closures sont créées à chaque fois qu'une fonction est créée, au moment de la création de la fonction.
Exemple :
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Ici, makeCounter retourne une fonction interne qui incrémente count. Même après que makeCounter a fini de s'exécuter, la fonction retournée a toujours accès à count. C'est une closure.
Comment fonctionnent les closures sous le capot
Lorsqu'une fonction est créée, elle stocke une référence à son environnement lexical (la chaîne de portée). Lorsque la fonction est invoquée, elle utilise cet environnement pour résoudre les variables. L'environnement est maintenu en vie aussi longtemps que la closure existe.
Utilisations pratiques des closures
Les closures ne sont pas seulement théoriques ; elles sont utilisées partout en JavaScript.
1. Confidentialité des données (Module Pattern)
Avant les modules ES6, les closures étaient utilisées pour créer des variables privées :
const counterModule = (function() {
let count = 0;
return {
increment: function() { count++; },
getCount: function() { return count; }
};
})();
counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined
Ici, count est privé et accessible uniquement via les méthodes retournées.
2. Gestionnaires d'événements et callbacks
Les closures sont couramment utilisées dans les gestionnaires d'événements pour accéder aux variables de la portée externe :
function setupButton() {
let clickCount = 0;
document.getElementById('myButton').addEventListener('click', function() {
clickCount++;
console.log(`Button clicked ${clickCount} times`);
});
}
La closure du gestionnaire d'événements se souvient de clickCount.
3. Fabriques de fonctions
Vous pouvez créer des fonctions avec des arguments prédéfinis :
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 10
Pièges courants et comment les éviter
Les closures peuvent entraîner un comportement inattendu si elles ne sont pas comprises.
Capture de variable dans une boucle
Utiliser var dans les boucles crée une seule liaison partagée :
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3
}, 100);
}
Tous les callbacks partagent le même i. Pour corriger, utilisez let (portée de bloc) ou un IIFE :
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2
}, 100);
}
Fuites de mémoire
Les closures gardent les variables en vie. Si une closure est attachée à un objet à longue durée de vie (comme un élément DOM), elle peut empêcher le ramasse-miettes. Supprimez toujours les écouteurs d'événements lorsqu'ils ne sont plus nécessaires.
Chaîne de portée et résolution des variables
Lorsque vous accédez à une variable, JavaScript parcourt la chaîne de portée : d'abord la portée actuelle, puis les portées externes, jusqu'à la portée globale. Si elle n'est pas trouvée, une ReferenceError est levée.
Exemple :
let a = 'global';
function outer() {
let b = 'outer';
function inner() {
let c = 'inner';
console.log(a, b, c); // all accessible
}
inner();
}
outer();
La chaîne de portée pour inner est : inner -> outer -> global.
Comparaison : var, let et const
| Mot-clé | Portée | Hoisting | Réassignable |
|---|---|---|---|
| var | Fonction | Oui (initialisé à undefined) | Oui |
| let | Bloc | Oui (mais non initialisé, TDZ) | Oui |
| const | Bloc | Oui (mais non initialisé, TDZ) | Non |
Comprendre ces différences est essentiel pour éviter les bugs liés à la portée.
FAQ
Qu'est-ce qu'une closure en JavaScript ?
Une closure est une fonction qui conserve l'accès à sa portée lexicale même lorsqu'elle est exécutée en dehors de cette portée. Elle permet à une fonction de « se souvenir » des variables de l'endroit où elle a été définie.
Pourquoi les closures sont-elles importantes ?
Les closures permettent la confidentialité des données, les fabriques de fonctions et les gestionnaires d'événements qui maintiennent un état. Elles sont fondamentales pour de nombreux patterns JavaScript.
Comment puis-je éviter les bugs liés aux closures ?
Utilisez let et const pour la portée de bloc, soyez attentif aux boucles et nettoyez les écouteurs d'événements pour éviter les fuites de mémoire.
Prêt à tester votre compréhension ? Essayez notre JSON Formatter pour expérimenter avec des extraits de code et voir comment les closures fonctionnent en temps réel.