Back

Comment fonctionnent les closures en JavaScript

Comment fonctionnent les closures en JavaScript

Vous avez écrit une fonction à l’intérieur d’une autre fonction, et d’une manière ou d’une autre, la fonction interne accède toujours aux variables de la fonction externe — même après que la fonction externe ait terminé son exécution. Ce comportement déroute de nombreux développeurs, mais il suit une règle simple : les closures capturent des liaisons, pas des valeurs.

Cet article explique comment fonctionnent les closures JavaScript, ce que signifie réellement la portée lexicale, et comment éviter les erreurs courantes qui découlent d’une mauvaise compréhension de ces mécanismes.

Points clés à retenir

  • Une closure est une fonction combinée avec son environnement lexical — les liaisons de variables qui existaient au moment de la création de la fonction.
  • Les closures capturent des liaisons (références), pas des valeurs, donc les mutations des variables capturées restent visibles.
  • Utilisez let ou const dans les boucles pour créer de nouvelles liaisons à chaque itération et éviter le problème classique des boucles.
  • Les problèmes de mémoire proviennent de la rétention de références inutiles, pas des closures elles-mêmes.

Qu’est-ce qu’une closure ?

Une closure est une fonction combinée avec son environnement lexical — l’ensemble des liaisons de variables qui existaient au moment de la création de la fonction. Chaque fonction en JavaScript forme une closure au moment de sa création.

function createGreeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}`
  }
}

const sayHello = createGreeter('Hello')
sayHello('Alice') // "Hello, Alice"

Lorsque createGreeter retourne, son contexte d’exécution se termine. Pourtant, la fonction retournée accède toujours à greeting. La fonction interne n’a pas copié la chaîne “Hello” — elle a conservé une référence à la liaison elle-même.

La portée lexicale en JavaScript expliquée

La portée lexicale signifie que l’accès aux variables est déterminé par l’endroit où les fonctions sont définies dans le code source, et non par l’endroit où elles s’exécutent. Le moteur JavaScript résout les noms de variables en remontant la chaîne de portée depuis la portée la plus interne vers l’extérieur.

const multiplier = 2

function outer() {
  const multiplier = 10
  
  function inner(value) {
    return value * multiplier
  }
  
  return inner
}

const calculate = outer()
calculate(5) // 50, pas 10

La fonction inner utilise multiplier de son environnement lexical — la portée où elle a été définie — indépendamment de toute variable globale portant le même nom.

Les closures capturent des liaisons, pas des instantanés

Une idée fausse courante est que les closures « figent » les valeurs des variables. Ce n’est pas le cas. Les closures conservent des références aux liaisons, donc les mutations restent visibles :

function createCounter() {
  let count = 0
  return {
    increment() { count++ },
    getValue() { return count }
  }
}

const counter = createCounter()
counter.increment()
counter.increment()
counter.getValue() // 2

Les deux méthodes partagent la même liaison count. Les modifications apportées par increment sont visibles pour getValue car elles référencent la même variable.

Le problème classique des boucles : var versus let

Cette distinction est particulièrement importante dans les boucles. Avec var, toutes les itérations partagent une seule liaison :

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// Affiche : 3, 3, 3

Chaque callback ferme sur le même i, qui vaut 3 lorsque les callbacks s’exécutent.

Avec let, chaque itération crée une nouvelle liaison :

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// Affiche : 0, 1, 2

La portée de bloc donne à chaque closure son propre i.

Modèles pratiques : fonctions factory et gestionnaires d’événements

Les closures permettent de créer des fonctions factory qui produisent des comportements spécialisés :

function createValidator(minLength) {
  return function(input) {
    return input.length >= minLength
  }
}

const validatePassword = createValidator(8)
validatePassword('secret') // false
validatePassword('longenough') // true

Les gestionnaires d’événements utilisent naturellement les closures pour conserver le contexte :

function setupButton(buttonId, message) {
  document.getElementById(buttonId).addEventListener('click', () => {
    console.log(message)
  })
}

Le callback conserve l’accès à message longtemps après le retour de setupButton.

Considérations mémoire : ce que les closures retiennent réellement

Les closures ne causent pas intrinsèquement de fuites mémoire. Les problèmes surviennent lorsque les fonctions conservent involontairement des références à de gros objets ou à des structures de données persistantes.

function processData(largeDataset) {
  const summary = computeSummary(largeDataset)
  
  return function() {
    return summary // Ne retient que summary, pas largeDataset
  }
}

Si votre closure n’a besoin que d’une petite partie des données, extrayez-la avant de créer la fonction interne. L’objet volumineux d’origine devient alors éligible au ramasse-miettes.

Les moteurs JavaScript modernes optimisent les closures de manière agressive. Elles constituent une fonctionnalité normale du langage, pas un problème de performance dans une utilisation typique. Le problème n’est pas les closures elles-mêmes — c’est de conserver des références dont vous n’avez pas besoin.

Construire un modèle mental fiable

Pensez aux closures de cette façon : lorsqu’une fonction est créée, elle capture une référence à sa portée environnante. Cette portée contient des liaisons — des connexions entre des noms et des valeurs — pas les valeurs elles-mêmes. La fonction peut lire et modifier ces liaisons tout au long de sa durée de vie.

Ce modèle explique pourquoi les mutations sont visibles, pourquoi let résout les problèmes de boucles, et pourquoi les closures fonctionnent à travers les frontières asynchrones. La fonction et son environnement lexical voyagent ensemble.

Conclusion

Les closures sont des fonctions regroupées avec leur environnement lexical. Elles capturent des liaisons, pas des valeurs, donc les modifications des variables capturées restent visibles. Utilisez let ou const dans les boucles pour créer de nouvelles liaisons à chaque itération. Les problèmes de mémoire proviennent de la rétention de références inutiles, pas des closures elles-mêmes.

Comprendre la portée et les closures en JavaScript vous donne une base pour raisonner sur la durée de vie des variables, l’encapsulation des données et le comportement des callbacks — des modèles que vous rencontrerez quotidiennement dans le développement frontend.

FAQ

Chaque fonction en JavaScript est techniquement une closure car elle capture son environnement lexical au moment de sa création. Le terme closure fait généralement référence aux fonctions qui accèdent à des variables d'une portée externe après que cette portée ait terminé son exécution. Une fonction qui n'utilise que ses propres variables locales ou des variables globales ne démontre pas de comportement de closure de manière significative.

Les closures ne causent pas intrinsèquement de fuites mémoire. Les problèmes surviennent lorsque les closures conservent involontairement des références à de gros objets ou à des structures de données qui devraient être collectées par le ramasse-miettes. Pour éviter cela, extrayez uniquement les données dont votre closure a besoin avant de créer la fonction interne. Les moteurs JavaScript modernes optimisent les closures de manière agressive, elles ne constituent donc pas un problème de performance dans une utilisation typique.

Cela se produit lorsque vous utilisez var dans les boucles car var a une portée de fonction, pas de bloc. Toutes les itérations partagent la même liaison, donc les callbacks voient la valeur finale lorsqu'ils s'exécutent. Corrigez cela en utilisant let à la place, qui crée une nouvelle liaison pour chaque itération. Chaque closure capture alors sa propre copie de la variable de boucle.

Oui. Les closures capturent des liaisons (références aux variables), pas des instantanés de valeurs. Si la variable capturée change, la closure voit la valeur mise à jour. C'est pourquoi plusieurs fonctions partageant la même closure peuvent communiquer via des variables partagées, comme on le voit dans le modèle compteur où les méthodes increment et getValue partagent la même liaison count.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay