I. Introduction▲
Ce sujet n'est pas nouveau et a été abordé maintes fois. Nous allons cependant essayer de l'aborder d'un point de vue théorique dans un premier temps et voir ensuite comment le JavaScript s'en occupe techniquement.
Il serait intéressant d'avoir pris connaissance en amont des deux tutoriels précédents dédiés à la chaîne des portées et à l'objet des variables qui aideront à la compréhension du présent tutoriel sans pour autant être indispensables à la compréhension globale.
II. Théorie générale▲
Avant d'aborder la discussion sur les fermetures en JavaScript, il semble important de définir un certain nombre de concepts de la théorie générale de la programmation fonctionnelle.
Comme vous le savez peut-être, dans les langages de programmation fonctionnelle (et JavaScript en est un), les fonctions sont des données. C'est-à-dire qu'elles peuvent être affectées à des variables, passées en tant qu'arguments dans les paramètres des fonctions ou être retournées par les fonctions. Ces fonctions ont des noms et des structures spéciales.
II-A. Définitions▲
Un argument fonctionnel (« functionnal argument » ou « funarg ») est un paramètre de fonction dont la valeur est elle-même une fonction.
L'argument fonctionnel de l'exemple ci-dessus est une expression de fonction passée à la fonction umbrella.
La fonction qui reçoit comme paramètre l'argument fonctionnel est appelée une fonction d'ordre supérieur (« higher-order function »).
Un autre nom donné à une fonction d'ordre supérieur est fonction fonctionnelle ou, plus près des mathématiques, un opérateur. Dans l'exemple ci-dessus, umbrella est donc une fonction d'ordre supérieur.
Comme mentionné plus haut, un argument fonctionnel peut-être non seulement passé à travers les paramètres des fonctions appelées, mais également retourné comme valeur par une autre fonction.
Une fonction qui retourne une autre fonction est appelée une fonction avec valeur fonctionnelle (« function valued »).
Une fonction pouvant être utilisée comme une donnée standard (c'est-à-dire être passée en tant qu'argument ou recevoir des arguments fonctionnels ou encore être retournée en tant que valeur fonctionnelle) est appelée une fonction de première classe. En JavaScript, toutes les fonctions sont des fonctions de première classe.
Une fonction se recevant elle-même via un paramètre est une fonction autoapplicative (« self-applicative ») :
et une fonction qui se retourne elle-même est, quant à elle, appelée une fonction autoréplicative (« self-replicative ») :
L'un des motifs les plus intéressants des fonctions autoréplicatives est leur forme déclarative fonctionnant avec un seul argument de collection au lieu d'accepter la collection elle-même :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
// fonction impérative qui
// accepte une collection
function starsList
(
modes) {
modes.forEach
(
function (
mode) {
console.log
(
mode);
}
);
}
// et s'utilisant ainsi
starsList
([
'jill'
,
'chris'
,
'barry'
]
);
// `jill`
// `chris`
// `barry`
// vs
// forme déclarative des
// fonction autoreplicative
function stars
(
mode) {
console.log
(
mode);
return stars;
// on retourne la fonction elle-même
}
// s'utilisant en *déclarant* les S.T.A.R.S
stars
(
'jill'
)
(
'chris'
)
(
'barry'
)
Cependant, dans la pratique, travailler avec des collections sera plus efficient et intuitif.
Les variables locales qui sont définies dans les arguments fonctionnels passés sont bien sûr accessibles lors de l'activation (appel) de la fonction. Et cela grâce à un objet des variables qui stocke les données du contexte et qui est créé à chaque fois en entrant dans le contexte :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
function umbrella
(
funarg) {
// activation locale de funarg
// la variable `localVar` est disponible
// à l'intérieur de `funarg`
funarg
(
10
);
// `20`
funarg
(
20
);
// `30`
}
umbrella
(
function (
localParam) {
var localVar =
10
;
console.log
(
localParam +
localVar);
}
);
Cependant, comme nous l'avons vu dans le tutoriel portant sur la chaîne des portées, les fonctions en JavaScript peuvent être encapsulées dans des fonctions parentes et utiliser des variables depuis des contextes parents. Quand une variable utilisée dans une fonction provient d'une fonction parente, cela conduit à une particularité connue sous le nom de problème de l'argument fonctionnel (« Funarg problem »).
II-B. Problème de l'argument fonctionnel▲
Dans un langage de programmation avec pile d'exécution, les variables locales des fonctions sont stockées dans la pile (« stack »). Cette pile est augmentée chaque fois qu'une fonction est appelée et ses variables et arguments sont stockés dans le niveau ajouté.
Après retour d'une fonction, les variables et arguments de cette fonction sont supprimés de la pile en même temps que le niveau associé. Ce modèle est une grosse restriction pour l'utilisation de fonctions en tant que valeurs fonctionnelles puisque la fonction qui stockait la valeur retournée n'est plus dans la pile.
Ce problème apparaît le plus souvent quand une fonction utilise des variables libres.
Une variable libre est une variable qui est utilisée par une fonction, mais qui n'est ni un paramètre de la fonction ni une variable locale de la fonction.
Dans cet exemple, la variable freeVar est libre pour la fonction stars.
Si ce système utilisait un modèle avec pile d'exécution pour stocker les variables locales, cela signifierait qu'au retour de la fonction biohazard toutes les variables auraient été supprimées de la pile. Et cela aurait causé une erreur lors de l'activation de stars depuis l'extérieur.
Cependant pour ce cas particulier, dans une implémentation orientée pile, retourner la fonction stars n'aurait pas été possible du tout, puisque stars est aussi locale à biohazard et aurait donc également été supprimée au retour de la fonction biohazard.
Un autre problème avec les objets fonctionnels est lié au passage de fonctions en tant qu'arguments dans un système avec une implémentation de portée dynamique.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
var z =
10
function zombi
(
) {
console.log
(
z)
}
zombi
(
) // `10` avec une portée statique ou une portée dynamique
(
function (
) {
var z =
20
zombi
(
) // `10` avec une portée statique, `20` avec une portée dynamique
}
)(
)
// et la même chose en passant `zombi`
// en tant qu'argument du premier paramètre `funarg`
(
function (
funarg) {
var z =
30
funarg
(
) // `10` avec une portée statique, `30` avec une portée dynamique
}
)(
zombi)
Nous voyons que dans un système avec une portée dynamique, la résolution des variables est gérée grâce à une pile dynamique de variables. Donc les variables libres sont cherchées dans la chaîne dynamique de l'activation courante, mise en place lors de la phase d'appel de la fonction, et non dans une portée (lexicale) statique qui est créée lors de la phase de création de la fonction.
Donc même si z existe (contrairement à l'exemple précédent où la variable locale aurait été supprimée de la pile), une question se pose : quelle valeur de z (c'est-à-dire z depuis quel contexte, depuis quelle portée) devrait être utilisée dans les différents appels de la fonction zombi ?
Ces deux cas de figure lèvent deux types de problèmes : comment faire fonctionner les valeurs fonctionnelles retournées depuis des fonctions (« upward funarg ») et comment faire fonctionner les arguments fonctionnels passés à des fonctions (« downward funarg ») ?
Pour résoudre ces problèmes (et leurs variantes), le concept de fermeture a été proposé.
II-C. Une fermeture▲
Une fermeture (« closure ») est la combinaison d'un pend de code et des données du contexte dans lequel ce pend de code est créé. Imageons cela avec le code suivant :
Même si le mot « lexical » est souvent omis quand on parle de portée lexicale, le fait est qu'une fermeture sauve les variables parentes dans un champ lexical dédié au pend de code, là où il est défini. Lors des prochaines activations de ce code, les variables libres seront cherchées à travers ce contexte lexical fermé (et ainsi comme nous l'avons vu dans l'exemple plus haut, la variable z sera toujours résolue à 10 pour une portée statique).
Dans la définition nous avons utilisé le terme générique de « pend de code », mais celui-ci peut être plus précis en fonction du langage. En JavaScript par exemple, le terme est complètement remplaçable par « fonction ». Mais cela n'est pas nécessairement le cas de tous les langages, par exemple en Ruby, où les fermetures peuvent être appliquées à autre chose que des fonctions.
Comme type d'implémentation pour le stockage de variables après la destruction d'un contexte, l'implémentation basée sur une pile ne convient pas du tout (car cela contredit la définition d'une structure basée sur une pile). Dans ce cas, les données gérées par la fermeture d'un contexte parent peuvent être sauvées dynamiquement en mémoire comme dans un tas (« heap »), c'est-à-dire dans une implémentation basée sur un tas. Une telle implémentation utilise un ramasse-miettes (« garbage collector ») et des références par comptage. Ces systèmes sont moins rapides que les systèmes basés sur une pile. Cependant, les implémentations peuvent toujours optimiser cela en vérifiant lors de la phase d'analyse d'une fonction quelles variables libres sont utilisées et décider en fonction de cela de les placer dans une pile ou dans un tas.
III. Les fermetures en JavaScript▲
Maintenant que nous avons discuté de la théorie, nous allons parler des fermetures (« closures ») spécifiquement dans le contexte du JavaScript. Il est nécessaire de noter ici que le JavaScript utilise uniquement une portée (lexicale) statique (alors que dans certains langages, comme en Perl, les variables peuvent être déclarées en utilisant des portées dynamiques ou statiques).
Techniquement, les variables d'un contexte parent sont sauvées dans la propriété interne [[Scope]] de la fonction. Aussi si vous souhaitez complètement comprendre [[Scope]] et la chaîne des portées, cela a été discuté en détail dans ce tutoriel. Ainsi les problématiques de fermetures devraient disparaître d'elles-mêmes.
En s'appuyant sur l'algorithme de création des fonctions, nous voyons que toutes les fonctions en JavaScript sont des fermetures, puisque toutes créent une chaîne des portées du contexte parent. Ce qu'il faut retenir étant qu'au moment où une fonction est activée (appelée), la portée parente est déjà sauvée depuis le moment de sa création.
Pour des questions d'optimisations, quand une fonction n'utilise pas de variables libres, l'implémentation peut ne pas sauver de chaîne des portées. Cependant rien n'est mentionné à ce propos dans la spécification ECMA-262-3. C'est donc une liberté prise au niveau de l'implémentation (et par l'algorithme technique) car « toutes les fonctions sauvent une chaîne des portées dans leur propriété interne [[Scope]] lors de la création ».
Plusieurs implémentations permettent un accès direct à cette portée fermée. Par exemple dans Rhino, une propriété non standard __parent__ correspond à la propriété interne [[Scope]][0] dont nous avons discuté dans le chapitre sur l'objet des variables :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
var GO =
this;
var t =
10
;
var virus = (
function (
) {
var g =
20
;
return function (
) {
console.log
(
g);
};
}
)(
);
virus
(
);
// `20`
console.log
(
virus.
__parent__.
g);
// `20`
virus.
__parent__.
g =
30
;
virus
(
);
// `30`
// Nous pouvons nous déplacer à travers la chaîne des portées jusqu'au bout
console.log
(
virus.
__parent__.
__parent__ ===
GO);
// `true`
console.log
(
virus.
__parent__.
__parent__.
t);
// `10`
III-A. Une valeur [[Scope]] pour tous▲
Il est nécessaire de noter que la propriété fermée [[Scope]] en JavaScript est le même objet pour plusieurs des fonctions internes créées dans le contexte parent. Cela signifie que la modification d'une variable dans une fermeture va se refléter lors de la lecture de cette variable depuis une autre fermeture.
En clair : toutes les fonctions internes partagent la même chaîne des portées parente.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
var tyran;
var nemesis;
function weapons
(
) {
var x =
1
;
tyran =
function (
) {
return ++
x;
};
nemesis =
function (
) {
return --
x;
};
x =
2
;
// affectation de AO(<weapons>)[`x`], qui est dans la propriété `[[Scope]]` de chaque fermeture
console.log
(
tyran
(
));
// `3`, via tyran.[[Scope]]
}
weapons
(
);
console.log
(
tyran
(
));
// `4`, via tyran.[[Scope]]
console.log
(
nemesis
(
));
// `3`, via nemesis.[[Scope]]
Il y a une erreur très répandue liée à cette fonctionnalité. Souvent les développeurs obtiennent un résultat qu'ils n'attendaient pas quand ils créent des fonctions dans des boucles, essayant d'associer à chaque fonction de la boucle une variable de comptage, s'attendant à ce que chaque fonction garde sa « propre » valeur.
Ce comportement est expliqué par l'exemple précédent. Une même portée liée au contexte est créée pour ces trois fonctions. Chaque fonction la référence dans sa propriété interne [[Scope]], et la variable k depuis cette portée parente peut-être facilement changée.
Regardez plutôt ce qu'il se passe étape par étape pour la chaîne des portées :
Phase d'entrée dans le contexte global :
2.
3.
4.
// activeGlobalContext.Scope = activeGlobalContext.AO
activeGlobalContext.
Scope =
[
{
data
:
<
référence à `data`
>,
k
:
undefined }
]
Phase d'exécution du contexte global :
2.
3.
4.
// activeGlobalContext.Scope = activeGlobalContext.AO
activeGlobalContext.
Scope =
[
{
data
:
[<...>],
k
:
3
}
]
Phase d'entrée de la fonction data[1] (par exemple) :
2.
3.
4.
5.
// activeFunctionContext.Scope = activeFunctionContext.AO + data[1].[[Scope]]
activeFunctionContext.
Scope =
[
{
x
:
undefined },
// objet d'activation courant `AO`
{
data
:
[<...>],
k
:
3
}
// l'objet global (`activeFunctionContext.AO`)
]
Phase d'exécution de la fonction data[1] (par exemple) :
2.
3.
4.
5.
6.
7.
8.
9.
// activeFunctionContext.Scope = activeFunctionContext.AO + data[1].[[Scope]]
activeFunctionContext.
Scope =
[
{
x
:
true },
{
data
:
[<...>],
k
:
3
}
]
// c.-à-d
data[
1
].[[
Scope]].
k ===
globalContext.
Scope.
k ===
`3`
Au moment de l'activation de la fonction, c'est la dernière valeur de k affectée qui s'affiche, c'est-à-dire 3.
Cela est dû au fait que toutes les variables sont créées avant l'exécution du code, c'est-à-dire lors de la phase d'entrée dans le contexte. Ce comportement est aussi connu sous le nom de hissage (« hoisting ») de variable.
La création d'un contexte fermé additionnel peut résoudre ce problème :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
var data =
[];
for (
var k =
0
;
k <
3
;
k++
) {
data[
k]
= (
function helper
(
x) {
return function (
) {
console.log
(
x);
};
}
)(
k);
// passage de la valeur `k` pour chaque objet des variables
}
// maintenant c'est le résultat souhaité
data[
0
](
);
// `0`
data[
1
](
);
// `1`
data[
2
](
);
// `2`
Regardons ce qu'il se passe dans ce cas.
En premier lieu, la fonction helper est créée et immédiatement activée avec comme premier argument k.
Puis, la valeur retournée par la fonction helper est aussi une fonction, contenant les éléments du tableau data original.
Cette technique produit les effets suivants : à l'activation, helper crée à chaque fois un nouvel objet d'activation qui contient le paramètre x, et la valeur de ce paramètre est la valeur de l'argument k passé.
Donc, les propriétés [[Scope]] de chaque fonction retournée sont les suivantes lors de la phase d'exécution :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
data[
0
].[[
Scope]]
===
[
{
x
:
0
},
{
data
:
[<...>],
k
:
3
}
]
data[
1
].[[
Scope]]
===
[
{
x
:
1
},
{
data
:
[...],
k
:
3
}
]
data[
2
].[[
Scope]]
===
[
{
x
:
2
},
{
data
:
[...],
k
:
3
}
]
Nous voyons maintenant que la propriété [[Scope]] des fonctions a une référence sur la valeur souhaitée via la variable x qui est capturée par la portée additionnelle créée.
Notons que les fonctions retournées gardent toujours leur référence à la variable k. La variable k garde toujours à travers toutes les fonctions la valeur 3.
Parfois les fermetures JavaScript incomplètes sont réduites au motif montré plus haut, avec la création de fonctions additionnelles de capture des valeurs désirées. D'un point de vue pratique, ce motif se doit d'exister, mais d'un point de vue théorique comme mentionné : toutes les fonctions en JavaScript sont des fermetures, et pas seulement dans ce cas de figure.
Le motif décrit plus haut n'est pas le seul utilisable. Pour conserver la valeur souhaitée de la variable k il est aussi possible, par exemple, d'utiliser cette approche :
Notons que ES6 standardise la portée de structure, qui peut être mise en place en utilisant les mots clés let ou const en tant que déclaration de variable. L'exemple ci-dessus peut alors simplement être réécrit ainsi :
III-B. Argument fonctionnel et return▲
Une autre fonctionnalité est le retour des fermetures. En JavaScript, une instruction avec le mot-clé return dans une fermeture rend le contrôle de flux depuis un contexte appelant.
Voici un exemple pour comprendre le comportement standard de return en JavaScript :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
function getElement
(
) {
[
1
,
2
,
3
]
.forEach
(
function (
element
) {
if (
element
%
2
==
0
) {
// retour de la fonction « forEach »,
//, mais pas retour de la fonction `getElement`
console.log
(
'trouvé: '
+
element
);
// trouvé: 2
return element
;
}
}
);
return null;
}
console.log
(
getElement
(
));
// `null`, et non `2`
Ainsi en JavaScript, lancer et attraper certaines exceptions peut aider :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
function getElement
(
) {
var $break =
{};
try {
[
1
,
2
,
3
]
.forEach
(
function (
element
) {
if (
element
%
2
===
0
) {
// « return » pour getElement depuis cette fermeture
console.log
(
'trouvé: '
+
element
);
// trouvé: 2
$break.
data =
element
;
throw $break;
}
}
);
}
catch (
e) {
if (
e ===
$break) {
return $break.
data;
}
}
return null;
}
console.log
(
getElement
(
));
// `2`
III-C. Théorie et exception▲
Comme nous l'avons noté, les développeurs réduisent souvent les fermetures à de simples fonctions internes retournées par leurs contextes parents. Ceci réduit les fermetures à être exploitables uniquement dans les fonctions anonymes.
Laissez-moi vous le dire à nouveau : toutes les fonctions, indépendamment de leur type (expressions nommées et anonymes ou déclarations), parce qu'elles possèdent une chaîne des portées, sont des fermetures.
Une exception à cette règle subsiste pour les fonctions créées via le constructeur Function dont la propriété [[Scope]] ne contient que l'objet global.
IV. Usage pratique des fermetures▲
Dans la pratique les fermetures permettent la création de structures élégantes, favorisant la personnalisation de différents calculs définis par un argument fonctionnel. En voici des exemples non exhaustifs.
IV-A. Argument fonctionnel▲
Voici un exemple avec la méthode sort des tableaux qui accepte en tant que premier paramètre un argument fonctionnel de « condition de tri » :
2.
3.
[
1
,
2
,
3
]
.sort
(
function (
a,
b) {
// ... conditions de tri de votre choix
}
);
Voici un autre exemple avec la méthode find. Il est parfois intéressant d'utiliser des fonctions de recherche en utilisant des arguments fonctionnels définissant les conditions de recherche :
IV-B. Association fonctionnelle▲
Voici un exemple de ce que l'on appelle l'association fonctionnelle (« mapping functionnals ») avec la méthode map d'un tableau. Celle-ci va associer à un nouveau tableau une valeur calculée à chaque élément :
IV-C. Boucle de fonction▲
Il est aussi intéressant d'autres fois d'appliquer les fonctions fonctionnelles, par exemple dans une méthode forEach qui applique des instructions pour chaque élément d'un tableau :
IV-D. apply et call▲
Au passage, les méthodes de fonction apply et call utilisent également des arguments fonctionnels. Nous avons déjà discuté de ces méthodes dans une note à propos de la valeur de this mais ici, nous allons voir leurs rôles avec les arguments fonctionnels ou fonctions appliquées en tant qu'argument (à une liste d'arguments avec apply et des positions d'argument avec call) :
IV-E. Appels différés▲
Un autre point important des fermetures est la possibilité des appels différés (« deferred calls ») :
IV-F. Fonction de rappel▲
Plus simplement, un cas d'usage répandu des fermetures est celui des fonctions de rappel (« callback functions ») :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
// ...
var x =
10
;
xmlHttpRequestObject.
onreadystatechange =
function (
) {
// la fonction de rappel est appelée en différé,
// quand les données sont prêtes.
// La variable `x` est ici disponible indépendamment
// du fait que lorsque le contexte interne existe,
// l'exécution du code externe est déjà finie.
console.log
(
x);
// `10`
};
// ...
IV-G. Encapsulations privées▲
Les fermetures servent également à « masquer » des variables dans une portée encapsulante lors de l'exécution d'instructions :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
var residentEvil =
{};
// initialisation
(
function (
object) {
var veronica =
10
;
object.
getVirus =
function _getVirus
(
) {
return veronica;
};
}
)(
residentEvil);
console.log
(
residentEvil.getVirus
(
));
// retourne la valeur `veronica` enfermée : `10`
console.log
(
veronica);
// « erreur : `veronica` n'est pas défini(e) »
V. Conclusion▲
Ce tutoriel vous en a dit plus à propos de la théorie générale des fermetures afin de mieux aborder son application en JavaScript même si le fait d'avoir étudié la chaîne des portées en amont nous a bien facilité la tâche. Nous entrerons prochainement dans le détail de la programmation orientée objet dans le domaine du JavaScript !
VI. Remerciements▲
Nous remercions Bruno Lesieur qui nous a autorisés à publier ce tutoriel.
Nous remercions également Laethy pour la mise au gabarit et Genthial pour la correction orthographique.