On a tendance à utiliser les mots « fonction » et « closure » de manière interchangeable. C’est une approximation pratique, mais elle masque une distinction qui explique une grande partie du comportement de JavaScript : pourquoi une variable « survit » à la fonction qui l’a déclarée, pourquoi deux compteurs créés par la même fabrique restent indépendants, ou comment fonctionnent réellement le currying et la mémoïsation. Une closure n’est pas une fonction. C’est une fonction plus quelque chose. Voyons précisément ce qu’est ce « quelque chose ».
Une fonction et son scope lexical
Une fonction encapsule une série d’instructions. Mais elle ne vit pas dans le vide : chaque fonction possède un scope lexical (champ lexical), c’est-à-dire l’environnement dans lequel ses variables sont résolues. Dans ce scope, on distingue deux catégories de variables.
Les variables locales, dites « bound »
Ce sont les variables issues des arguments de la fonction ou déclarées directement dans son corps. Elles sont liées à la fonction au sens où elles n’existent que dans son périmètre.
// x, y, z sont des variables "bound"
function add(x, y) {
const z = 1
}
Ces variables sont gardées en mémoire le temps de l’exécution de la fonction, puis libérées, du moins en l’absence de closure. Leur durée de vie est, par défaut, celle de l’appel.
Les variables non locales, dites « free »
À l’inverse, une variable « free » est une variable utilisée par la fonction mais définie dans un scope extérieur au sien. La fonction la consomme sans la déclarer elle-même.
function createService() {
let count = 0
return {
// "count" est une variable "free" pour incrementCounter
incrementCounter() { count++ }
}
}
Ici, count est déclarée dans createService, mais elle est utilisée par incrementCounter, une fonction imbriquée. Du point de vue de incrementCounter, count est une variable « free ». Et c’est précisément autour de ces variables libres que tout se joue.
Qu’est-ce qu’une closure, alors ?
La définition tient en une formule :
Une closure = une fonction + un contexte.
Une closure combine une fonction avec son contexte d’exécution. Ce contexte est un environnement qui conserve en mémoire les variables « free » nécessaires à l’exécution de la fonction. Autrement dit, une closure garde des références vers ces variables libres, de manière à pouvoir les réutiliser lors d’un appel ultérieur de la fonction, même si le scope qui les a déclarées a, en apparence, fini son exécution.
On dit que la closure « capture » ces variables. Ces références ne flottent pas dans le vide : elles sont stockées dans un contexte rattaché à la fonction.
Prenons un exemple canonique :
function outer() {
// variable "free" a, définie dans le scope de la fonction "outer"
const a = 10
return function inner() {
// "a" est capturé par la closure
return a
}
}
// "someClosure" est en fait une closure qui contient les références
// utilisées de la fonction "outer" depuis la fonction "inner"
const someClosure = outer() // Closure (outer) {a: 10}
Au moment où outer() retourne, son appel est terminé. Dans un monde sans closures, a serait libérée et perdue. Mais inner référence a : la closure capture donc cette variable et la maintient en vie. À chaque exécution de someClosure, c’est le contexte de la closure qui est consulté pour résoudre a et retourner 10.
C’est la nuance essentielle : inner, en tant que fonction, n’est qu’un bloc d’instructions. someClosure, en tant que closure, est ce bloc d’instructions accompagné de l’environnement ({a: 10}) qui lui permet de fonctionner hors du scope d’origine.
Comment une closure est-elle implémentée ?
En pratique, l’implémentation d’une closure varie selon les runtimes et les langages. Mais regarder « sous le capot » d’un moteur concret aide à dissiper la magie.
Dans V8, le moteur JavaScript de Chrome et de Node.js, une fonction est représentée par une instance de JSFunction. Lorsqu’une fonction capture des variables libres, V8 lui rattache un objet Context : c’est cette structure qui stocke l’ensemble des variables capturées et qui survit tant que la closure reste accessible.
Ce détail d’implémentation a une conséquence directe et souvent contre-intuitive : tant qu’une closure est référencée, son contexte, et donc les variables qu’il retient, ne peut pas être collecté par le ramasse-miettes. C’est ce qui rend les closures si puissantes pour conserver un état, mais aussi ce qui peut conduire à des fuites mémoire si l’on capture par mégarde des objets volumineux dont on n’a plus besoin.
À quoi sert une closure ?
La capture de contexte n’est pas une curiosité académique : elle sous-tend une grande partie des patterns que l’on manipule au quotidien en JavaScript. Quelques exemples de concepts qui reposent directement sur les closures :
- le currying ;
- l’application partielle ;
- les callbacks et les event listeners ;
- la mémoïsation ;
- les fonctions d’ordre supérieur (high-order functions).
Chacun de ces mécanismes exploite la même idée fondamentale : une fonction qui « se souvient » de l’environnement dans lequel elle a été créée. C’est exactement ce que nous explorerons dans le prochain volet de cette série, consacré aux cas d’usage concrets des closures.
Conclusion
Retenez la distinction : une fonction est une série d’instructions dotée d’un scope lexical ; une closure est cette fonction associée au contexte qui retient ses variables « free ». La fonction décrit quoi exécuter ; le contexte fournit avec quoi l’exécuter, même longtemps après que le scope d’origine a disparu.
Comprendre cette nuance, ce n’est pas chipoter sur du vocabulaire. C’est saisir pourquoi l’état persiste, comment se construisent les abstractions fonctionnelles, et où guetter les fuites mémoire. Une fois la mécanique de capture intériorisée, des pans entiers du langage, du currying aux callbacks, cessent d’être des recettes pour devenir des conséquences évidentes d’un même principe.