La portée & les closures
Premier pilier : où vivent les variables (la portée) et comment une fonction « se souvient » de son environnement (la closure).
Le troisième chapitre de Get Started creuse les racines de JavaScript : les mécanismes de bas niveau qui sous-tendent presque chaque ligne que nous écrivons. Le premier de ces piliers est aussi le plus discret, au point qu'on l'utilise tous les jours sans le nommer. La portée (scope) décide où une variable existe et reste accessible. La fermeture (closure), sa conséquence directe, explique comment une fonction continue de « se souvenir » de variables même longtemps après que le code qui les a créées a fini de s'exécuter.
Kyle Simpson le formule sans détour : la closure est l'un des mécanismes les plus répandus de la programmation, peut-être aussi fondamental que les variables ou les boucles. Pourtant elle semble cachée, presque magique, et on en parle souvent en termes trop abstraits ou trop vagues pour qu'on en saisisse l'essence. L'objectif de ce chapitre est de la rendre concrète, parce que sa présence — ou son absence — est parfois la cause directe d'un bug ou d'un problème de performance.
La portée : où vivent les variables
La portée (scope) est la région d'un programme où une variable est accessible. Ce n'est pas un détail d'organisation : c'est l'ensemble des règles qui contrôlent comment une référence à une variable est résolue. Chaque fois que le moteur rencontre un nom comme count, il doit décider de quelle variable il s'agit, et c'est la portée qui tranche.
Le point capital, sur lequel Simpson revient dans tout le Book 2, est que la portée de JavaScript est lexicale (lexical scope). « Lexicale » signifie qu'elle est déterminée par l'emplacement du code, là où vous écrivez vos fonctions et vos blocs, et non par la manière dont le programme s'exécute à un instant donné. La portée est donc fixée tôt, à la « compilation », avant même que le premier appel ait lieu.
Note
La portée est statique : c'est un ensemble fixe de variables, disponibles au moment et à l'endroit où vous définissez une fonction. C'est ce qui l'oppose au this, dont la valeur est dynamique et dépend de la façon dont la fonction est appelée. Confondre les deux est l'une des grandes sources d'erreurs en JS.
Des portées imbriquées
Chaque fonction crée sa propre portée. Et comme on écrit des fonctions à l'intérieur d'autres fonctions (ou de blocs), ces portées s'imbriquent les unes dans les autres, comme des poupées russes. Une fonction interne peut accéder aux variables de sa portée, mais aussi à celles de toutes les portées qui l'englobent.
var marque = "globale";
function exterieure() {
var milieu = "exterieure";
function interieure() {
var local = "interieure";
// La fonction interne voit les trois.
console.log(local, milieu, marque);
}
interieure(); // interieure exterieure globale
}
exterieure(); La résolution d'un nom se fait toujours de l'intérieur vers l'extérieur. Quand interieure lit milieu, le moteur cherche d'abord dans la portée de interieure : rien. Il remonte alors d'un cran, dans la portée de exterieure : trouvé, on s'arrête. S'il n'avait rien trouvé, il aurait continué jusqu'à la portée globale, puis aurait abandonné. La recherche s'arrête à la première correspondance, ce qui explique le phénomène d'occultation (shadowing) : une variable interne portant le même nom qu'une variable externe masque cette dernière.
La closure : se souvenir de sa portée
Voici la définition pragmatique et concrète que donne Simpson, qu'il vaut mieux mémoriser telle quelle :
La closure, c'est quand une fonction se souvient et continue d'accéder à des variables extérieures à sa portée, même lorsque cette fonction est exécutée dans une portée différente.
Deux caractéristiques découlent de cette définition. Premièrement, la closure est une propriété des fonctions. Les objets n'ont pas de closure, les fonctions oui. Deuxièmement, pour observer une closure, il faut exécuter la fonction dans une portée différente de celle où elle a été définie — typiquement la renvoyer puis l'appeler ailleurs.
Voici l'exemple canonique : une fonction-fabrique (factory) qui en renvoie une autre.
function greeting(msg) {
return function who(name) {
console.log(`${msg}, ${name}!`);
};
}
var hello = greeting("Hello");
var howdy = greeting("Howdy");
hello("Kyle"); // Hello, Kyle!
hello("Sarah"); // Hello, Sarah!
howdy("Grant"); // Howdy, Grant! Décortiquons ce qui se passe. L'appel à greeting(..) crée une instance de la fonction interne who(..) ; cette fonction se ferme sur (closes over) la variable msg, le paramètre de la portée externe. Quand who est renvoyée, sa référence est stockée dans hello. On rappelle ensuite greeting(..) une seconde fois : une nouvelle instance de who est créée, avec une nouvelle closure sur un nouveau msg, renvoyée vers howdy.
Normalement, quand greeting(..) finit de s'exécuter, on s'attendrait à ce que toutes ses variables soient nettoyées par le ramasse-miettes (garbage collector) — tous ces msg devraient disparaître. Mais ils ne disparaissent pas. La raison est la closure : tant que les instances internes restent vivantes (référencées par hello et howdy), leurs closures préservent les variables msg correspondantes.
À retenir
Une closure n'est pas un instantané (snapshot) de la valeur au moment de la création. C'est un lien direct vers la variable elle-même, maintenue en vie. La closure peut donc observer — et même provoquer — les mises à jour de cette variable au fil du temps. Retenez ce point : il explique presque tout ce qui suit.
Un état privé : le compteur
Comme la closure capture la variable vivante et non une copie figée, on peut s'en servir pour conserver et faire évoluer un état privé (private state) que personne d'autre ne peut atteindre.
function counter(step = 1) {
var count = 0;
return function increaseCount() {
count = count + step;
return count;
};
}
var incBy1 = counter(1);
var incBy3 = counter(3);
incBy1(); // 1
incBy1(); // 2
incBy3(); // 3
incBy3(); // 6
incBy3(); // 9 Chaque instance de increaseCount se ferme sur deux variables de la portée de counter(..) : count et step. La valeur de step ne change pas, mais count est mise à jour à chaque appel — et puisque la closure porte sur la variable et non sur un instantané de sa valeur, ces mises à jour sont préservées d'un appel au suivant. La variable count est totalement inaccessible depuis l'extérieur : aucune autre portion du code ne peut la lire ni la modifier directement. C'est l'encapsulation par closure, sans classe ni mot-clé private.
Le piège classique : var dans une boucle
C'est ici que la nature « variable vivante, pas instantané » de la closure surprend tout le monde au moins une fois. Imaginez qu'on veuille associer à plusieurs minuteurs l'index de leur tour de boucle.
// ❌ Avant : une seule variable i partagée par tous.
for (var i = 0; i < 3; i++) {
setTimeout(function onTimer() {
console.log(`i vaut ${i}`);
}, 100);
}
// i vaut 3
// i vaut 3
// i vaut 3 Le résultat déroute : on attendait 0, 1, 2 et on obtient 3 trois fois. La raison est limpide une fois la définition assimilée. Avec var, il n'existe qu'une seule variable i pour toute la boucle. Les trois fonctions onTimer se ferment toutes sur cette même variable. Quand les minuteurs se déclenchent (après la fin de la boucle), i vaut déjà 3. Les closures n'ont pas capturé 0, 1, 2 : elles partagent toutes le même lien vers le même i, dont la valeur finale est 3.
// ✅ Après : let crée une liaison par itération.
for (let i = 0; i < 3; i++) {
setTimeout(function onTimer() {
console.log(`i vaut ${i}`);
}, 100);
}
// i vaut 0
// i vaut 1
// i vaut 2 Le seul changement est var remplacé par let. Mais il change tout : let est à portée de bloc, et la boucle for lui réserve un traitement spécial — chaque itération reçoit sa propre nouvelle liaison (binding) de i. Il y a donc trois variables i distinctes, chacune fixée à 0, 1, puis 2. Chaque onTimer se ferme sur la sienne. Le comportement « surprenant » disparaît, non par magie, mais parce que la closure capture désormais trois variables différentes au lieu d'une seule.
Attention
La règle « la closure capture la variable, pas la valeur » est exacte dans les deux cas. La différence ne vient pas de la closure mais du nombre de variables créées : une seule avec var, une par itération avec let. Comprendre cette nuance, c'est comprendre la closure pour de bon.
La closure au quotidien : callbacks et événements
Si la closure paraît exotique présentée seule, elle est en réalité partout dès qu'on touche au code asynchrone. Le cas le plus courant est le callback (callback) : on passe une fonction qui sera appelée plus tard, et celle-ci doit se souvenir du contexte d'origine.
function getSomeData(url) {
ajax(url, function onResponse(resp) {
console.log(`Réponse (de ${url}) : ${resp}`);
});
}
getSomeData("https://some.url/wherever");
// Réponse (de https://some.url/wherever) : ... La fonction interne onResponse(..) se ferme sur url. Même si getSomeData(..) se termine immédiatement, la variable url est maintenue en vie dans la closure aussi longtemps qu'il le faut — jusqu'à ce que la requête Ajax revienne, peut-être plusieurs secondes plus tard, et déclenche onResponse(..). Sans closure, l'asynchrone serait presque impraticable : la fonction de rappel n'aurait aucun moyen de retrouver les données du moment où elle a été créée.
Simpson note aussi un point souvent ignoré : la portée externe n'a pas besoin d'être une fonction. C'est généralement le cas, mais il suffit qu'au moins une variable d'une portée externe soit lue par une fonction interne. Un bloc de boucle convient parfaitement.
// `buttons` est une liste d'éléments du DOM.
for (let [idx, btn] of buttons.entries()) {
btn.addEventListener("click", function onClick() {
console.log(`Clic sur le bouton (${idx}) !`);
});
} Comme la boucle utilise let, chaque itération obtient ses propres variables idx et btn à portée de bloc, et crée une nouvelle fonction onClick. Chaque onClick se ferme sur son idx, conservé aussi longtemps que le gestionnaire reste attaché au bouton. Au clic, chaque gestionnaire affiche donc l'index qui lui est propre. Et, une dernière fois pour ancrer l'idée : cette closure porte sur la variable idx, pas sur la valeur (0, 1…) qu'elle contenait à un instant donné.
Une fonction partiellement configurée
Un autre usage très pratique de la fabrique à closure est de préconfigurer une fonction : on fige certains paramètres une fois pour toutes, et on renvoie une fonction plus spécialisée qui n'attend plus que le reste.
function multiplierPar(facteur) {
return function appliquer(valeur) {
return valeur * facteur;
};
}
var doubler = multiplierPar(2);
var tripler = multiplierPar(3);
doubler(10); // 20
tripler(10); // 30 doubler et tripler sont la même fonction appliquer, mais chacune s'est fermée sur un facteur différent. On obtient deux fonctions spécialisées sans réécrire la logique. C'est exactement le principe derrière la mémoïsation (memoization), où une closure conserve un cache de résultats déjà calculés, ou derrière les nombreuses bibliothèques de programmation fonctionnelle : la closure y est le mécanisme qui permet de « se souvenir » de configurations passées.
Astuce
Pour repérer une closure dans du code, posez-vous deux questions. La fonction interne lit-elle une variable déclarée plus haut, dans une portée englobante ? Et cette fonction survit-elle à sa portée d'origine (renvoyée, passée en callback, attachée comme gestionnaire) ? Si oui aux deux, il y a closure — et donc des variables maintenues en vie qu'il faut avoir en tête.
Pourquoi cela compte
La closure n'est pas un truc avancé réservé aux experts : c'est, selon Simpson, l'un des motifs les plus présents et les plus importants de tout langage, et plus encore en JavaScript, où il est difficile d'imaginer faire quoi que ce soit d'utile sans en tirer parti d'une façon ou d'une autre. État privé, callbacks asynchrones, gestionnaires d'événements, fonctions préconfigurées, mémoïsation : tous reposent sur le même mécanisme.
L'intérêt de creuser ce « pourquoi » dépasse la simple curiosité. Reconnaître où une closure agit vous évite des bugs (la boucle var), vous aide à comprendre pourquoi de la mémoire reste occupée, et vous donne un outil d'encapsulation puissant. C'est aussi le bon état d'esprit pour la suite : apprendre JavaScript en profondeur, c'est sans cesse demander « pourquoi ? » devant ce qui semble magique, jusqu'à ce que la magie devienne mécanique.
À retenir
- La portée (scope) est lexicale : elle est déterminée par l'emplacement de votre code, fixée tôt, et statique — à ne pas confondre avec le
this, lui dynamique. - Les portées s'imbriquent : une fonction interne accède aux variables externes ; la résolution va de l'intérieur vers l'extérieur et s'arrête à la première correspondance.
- Une closure, c'est une fonction qui se souvient de variables extérieures à sa portée, même exécutée ailleurs et plus tard. C'est une propriété des fonctions, pas des objets.
- La closure capture la variable vivante, pas un instantané : elle voit les modifications dans le temps — d'où l'état privé d'un compteur, et le piège de
vardans une boucle corrigé parlet(une liaison par itération). - Usages réels omniprésents : encapsulation d'un état privé, callbacks asynchrones, gestionnaires d'événements, fonctions partiellement configurées, mémoïsation.
- Demandez « pourquoi ? » : reconnaître la présence d'une closure prévient des bugs et clarifie l'usage mémoire ; la magie n'est qu'un mécanisme qu'on n'a pas encore disséqué.