Ouvrir la navigation secondaire
Précédent Complexité algorithmique et notation Grand O
Suivant Quelques algorithmes de tri en Python

Les fermetures en JavaScript

Ces notes sont en grande partie une simple reformulation d'une partie d'un article en guise de mémo, traduit en Français ici.

Quand un langage de programmation a des fonctions de première classe, ça veut dire que ces fonctions peuvent entre autres :

  • être passées en tant qu'arguments à d'autres fonctions, on parle d'arguments fonctionnels (funargs)
  • être retournées par d'autres fonctions, on parle de valeurs fonctionnelles (functional values)

Il existe 2 problèmes liés aux arguments fonctionnels et aux valeurs fonctionnelles, généralisés sous le nom de funarg problem.

Pour y remédier, un langage de programmation a la possibilité d'implémenter le concept de fermeture (closure).

Problème du funarg ascendant (upward funarg problem)

Ce problème apparaît quand une fonction est retournée depuis une autre fonction et qu'elle utilise des variables libres :

const outer = () => {
    const a = 10
    const inner = b => a * b
    return inner
}

const tenXify = outer()
tenXify(10)

La fonction retournée inner() utilise la variable a déclarée dans le corps de outer().

Du point de vue de inner(), a est une variable libre : elle n'est ni un de ses paramètres, ni une de ses variables locales.

Comment fait inner() pour accéder à la valeur de a alors que le contexte d'exécution de outer() n'existe plus ?

En fait, inner() sauvegarde la portée de son parent en plus de sa propre portée au moment de sa création.

Problème du funarg descendant (downward funarg problem)

Ce problème apparaît quand il y a une ambiguïté lors de la résolution d'un identifiant :

let x = 10

const foo = () => {
    console.log(x)
}

const bar = (funArg) => {
    let x = 20
    funArg()
}

bar(foo)  // 10, et non 20

Depuis quelle portée doit-on résoudre la valeur de x au moment de l'appel de bar(foo) ? Depuis la portée enregistrée lors de la création de foo() ou depuis la portée de l'appelant bar ?

Pour éviter toute ambiguïté, il a été décidé que la portée utilisée serait celle enregistrée au moment de la création.

Solution : fermeture (closure)

Une fermeture permet à une fonction de résoudre ses variables libres à l’aide de ses portées enregistrées : elle est la combinaison de la portée d'une fonction et de celles de ses portées parentes enregistrées au moment de leurs créations.

Le mot fermeture exprime l'idée d'enfermer les variables qui pourraient être nécessaires à une fonction.

Comme chaque fonction enregistre sa portée lors de sa création, théoriquement toutes les fonctions en JavaScript sont des closures. Du coup quand on vous demande quels sont les usages des fermetures, la question est plutôt mal posée :)

Bonus : partage de portée

Plusieurs fonctions peuvent avoir la même portée parente :

var data = []

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    return console.log(k)
  }
}

data[0]()  // 3, pas 0
data[1]()  // 3, pas 1
data[2]()  // 3, pas 2

Dans cet exemple, la boucle for assigne une fonction à chaque élément du tableau data.

Au sein de cette fonction, k est une variable libre.

Comment déterminer la valeur de k au moment de l'exécution ?

JavaScript regarde dans la chaîne des portées de la fonction.

Il trouve k dans la portée parente.

Mais la portée parente est en fait la portée globale, k en fait partie car elle a été déclarée via le mot clé var.

Or chaque fonction stockée dans le tableau data partage cette portée, dans laquelle k possède sa dernière valeur assignée.

Du coup, k a toujours la même valeur.

Pour fixer ce problème de portée partagée, il était d'usage de forcer l'ajout d'un contexte fermé additionnel avec une IIFE par exemple.

Maintenant, il suffit tout simplement de limiter la portée de k au bloc parent via le mot clé let :

var data = []

for (let k = 0; k < 3; k++) {
  data[k] = function () {
    return console.log(k)
  }
}

data[0]()  // 0
data[1]()  // 1
data[2]()  // 2