Les callbacks, les gestionnaires d’événements, les fonctions d’ordre supérieur peuvent accéder aux variables de portée externe grâce aux fermetures. Les closures sont importantes en programmation fonctionnelle, et sont souvent demandées lors de l’entretien de codage JavaScript.
Bien qu’elles soient utilisées partout, les closures sont difficiles à appréhender. Si vous n’avez pas eu votre moment « Aha ! » dans la compréhension des closures, alors ce post est pour vous.
Je commencerai par les termes fondamentaux : scope et scope lexical. Puis, après avoir saisi les bases, il vous suffira d’une étape pour enfin comprendre les closures.
Avant de commencer, je vous suggère de résister à l’envie de sauter les sections sur le scope et lexical scope. Ces concepts sont cruciaux pour les fermetures, et si vous les comprenez bien, l’idée de fermeture devient évidente.
1. Le scope
Lorsque vous définissez une variable, vous voulez qu’elle soit accessible dans certaines limites. Par exemple, une variable result
a du sens pour exister au sein d’une fonction calculate()
, en tant que détail interne. En dehors de la , la variable result
est inutile.
L’accessibilité des variables est gérée par la portée. Vous êtes libre d’accéder à la variable définie dans sa portée. Mais en dehors de cette portée, la variable est inaccessible.
En JavaScript, une portée est créée par une fonction ou un bloc de code.
Voyons comment la portée affecte la disponibilité d’une variable count
. Cette variable appartient à une portée créée par la fonction foo()
:
function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined
count
est librement accessible dans la portée de foo()
.
Cependant, en dehors de la portée de foo()
count
est inaccessible. Si vous essayez malgré tout d’accéder à count
depuis l’extérieur, JavaScript lance ReferenceError: count is not defined
.
En JavaScript, la portée dit : si vous avez défini une variable à l’intérieur d’une fonction ou d’un bloc de code, alors vous ne pouvez utiliser cette variable que dans cette fonction ou ce bloc de code. L’exemple ci-dessus démontre ce comportement.
Maintenant, voyons une formulation générale:
Le scope est une politique spatiale qui régit l’accessibilité des variables.
Une propriété immédiate apparaît : le scope isole les variables. C’est génial car différents scopes peuvent avoir des variables avec le même nom.
Vous pouvez réutiliser des noms de variables communs (count
index
current
value
, etc) dans différents scopes sans collisions.
foo()
et bar()
les scopes des fonctions ont leurs propres variables, mais de même nom, count
:
function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0}function bar() { // "bar" function scope let count = 1; console.log(count); // logs 1}foo();bar();
count
les variables des scopes des fonctions foo()
et bar()
n’entrent pas en collision.
2. Emboîtement des scopes
Jouons un peu plus avec les scopes, et mettons un scope dans un autre.
La fonction innerFunc()
est imbriquée dans une fonction externe outerFunc()
.
Comment les 2 scopes de fonction interagiraient-ils entre eux ? Puis-je accéder à la variable outerVar
de outerFunc()
à partir du scope innerFunc()
?
Essayons cela dans l’exemple:
function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
En effet, la variable outerVar
est accessible à l’intérieur du scope innerFunc()
. Les variables de la portée extérieure sont accessibles à l’intérieur de la portée intérieure.
Maintenant vous savez 2 choses intéressantes :
- Les scope peuvent être imbriqués
- Les variables du scope externe sont accessibles à l’intérieur du scope interne
3. La portée lexicale
Comment JavaScript comprend-il que outerVar
à l’intérieur de innerFunc()
correspond à la variable outerVar
de outerFunc()
?
C’est parce que JavaScript met en œuvre un mécanisme de scoping nommé scoping lexical (ou scoping statique). Le scoping lexical signifie que l’accessibilité des variables est déterminée par la position des variables dans le code source à l’intérieur des scopes d’imbrication.
Plus simplement, le scoping lexical signifie qu’à l’intérieur du scope interne, vous pouvez accéder aux variables de ses scopes externes.
Il est appelé lexical (ou statique) parce que le moteur détermine (au moment de la lexie) l’imbrication des scopes juste en regardant le code source JavaScript, sans l’exécuter.
Voici comment le moteur comprend le bout de code précédent :
- Je vois que vous définissez une fonction
outerFunc()
qui a une variableouterVar
. Bien. - À l’intérieur de la
outerFunc()
, je vois que vous définissez une fonctioninnerFunc()
. - À l’intérieur du
innerFunc()
, je peux voir une variableouterVar
sans déclaration. Comme j’utilise le scoping lexical, je considère que la variableouterVar
à l’intérieur deinnerFunc()
est la même variable queouterVar
deouterFunc()
.
L’idée distillée de la portée lexicale:
La portée lexicale est constituée de portées extérieures déterminées statiquement.
Par exemple :
const myGlobal = 0;function func() { const myVar = 1; console.log(myGlobal); // logs "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // logs "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc();}func();
Le champ lexical de innerOfInnerOfFunc()
est constitué des scopes de innerOfFunc()
func()
et du scope global (le scope le plus externe). Dans innerOfInnerOfFunc()
, vous pouvez accéder aux variables de la portée lexicale myInnerVar
myVar
et myGlobal
.
La portée lexicale de innerFunc()
se compose de func()
et de la portée globale. Dans innerOfFunc()
, vous pouvez accéder aux variables de la portée lexicale myVar
et myGlobal
.
Enfin, la portée lexicale de func()
est constituée uniquement de la portée globale. Dans func()
, vous pouvez accéder à la variable de portée lexicale myGlobal
.
4. La fermeture
Ok, le scope lexical permet d’accéder aux variables de manière statique des scopes externes. Il ne reste plus qu’une étape jusqu’à la fermeture !
Regardons à nouveau l’exemple outerFunc()
et innerFunc()
:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Dans le scope innerFunc()
, on accède à la variable outerVar
depuis le scope lexical. C’est déjà connu.
Notez que l’invocation de innerFunc()
se fait à l’intérieur de son scope lexical (le scope de outerFunc()
).
Faisons une modification : innerFunc()
pour être invoqué en dehors de son champ lexical (en dehors de outerFunc()
). Est-ce que innerFunc()
serait toujours capable d’accéder à outerVar
?
Faisons les ajustements nécessaires au bout de code :
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc();
Maintenant innerFunc()
est exécuté en dehors de son scope lexical. Et ce qui est important :
innerFunc()
a toujours accès à outerVar
depuis son scope lexical, même en étant exécuté en dehors de son scope lexical.
En d’autres termes, innerFunc()
referme (a.k.a. capture, se souvient) la variable outerVar
de sa portée lexicale.
En d’autres termes, innerFunc()
est une fermeture car elle se referme sur la variable outerVar
de sa portée lexicale.
Vous avez fait le dernier pas pour comprendre ce qu’est une fermeture :
La fermeture est une fonction qui accède à sa portée lexicale même exécutée en dehors de sa portée lexicale.
Plus simplement, la fermeture est une fonction qui se souvient des variables de l’endroit où elle est définie, indépendamment de l’endroit où elle est exécutée plus tard.
Une règle empirique pour identifier une fermeture : si vous voyez dans une fonction une variable étrangère (non définie à l’intérieur de la fonction), il est fort probable que cette fonction soit une fermeture car la variable étrangère est capturée.
Dans l’extrait de code précédent, outerVar
est une variable étrangère à l’intérieur de la fermeture innerFunc()
capturée à partir de outerFunc()
scope.
Poursuivons avec des exemples qui montrent pourquoi la fermeture est utile.
5. Exemples de fermetures
5.1 Gestionnaire d’événements
Affichons le nombre de fois qu’un bouton est cliqué:
let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});
Ouvrir la démo et cliquer sur le bouton. Le texte se met à jour pour indiquer le nombre de clics.
Lorsque le bouton est cliqué, handleClick()
est exécuté quelque part à l’intérieur du code DOM. L’exécution se produit loin du lieu de définition.
Mais étant une fermeture, handleClick()
capture countClicked
de la portée lexicale et la met à jour lorsqu’un clic se produit. Plus encore, myText
est également capturé.
5.2 Callbacks
La capture des variables de la portée lexicale est utile dans les callbacks.
Une setTimeout()
callback:
const message = 'Hello, World!';setTimeout(function callback() { console.log(message); // logs "Hello, World!"}, 1000);
La callback()
est une fermeture car elle capture la variable message
.
Une fonction itérative pour forEach()
:
let countEven = 0;const items = ;items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; }});countEven; // => 2
La iterator
est une fermeture car elle capture la variable countEven
.
5.3 Programmation fonctionnelle
La fermeture se produit lorsqu’une fonction renvoie une autre fonction jusqu’à ce que les arguments soient entièrement fournis.
Par exemple :
function multiply(a) { return function executeMultiply(b) { return a * b; }}const double = multiply(2);double(3); // => 6double(5); // => 10const triple = multiply(3);triple(4); // => 12
multiply
est une fonction curée qui renvoie une autre fonction.
La curie, un concept important de la programmation fonctionnelle, est également possible grâce aux closures.
executeMultiply(b)
est une fermeture qui capture a
de sa portée lexicale. Lorsque la fermeture est invoquée, la variable capturée a
et le paramètre b
sont utilisés pour calculer a * b
.
6. Conclusion
Le scope est ce qui règle l’accessibilité des variables en JavaScript. Il peut y avoir une portée de fonction ou une portée de bloc.
Le scope lexical permet à un scope de fonction d’accéder statiquement aux variables des scopes extérieurs.
Enfin, une fermeture est une fonction qui capture les variables de son scope lexical. En termes simples, la fermeture se souvient des variables de l’endroit où elle est définie, peu importe où elle est exécutée.
Les closures capturent des variables à l’intérieur des gestionnaires d’événements, des callbacks. Ils sont utilisés dans la programmation fonctionnelle. D’ailleurs, on pourrait vous demander comment fonctionnent les closures lors d’un entretien d’embauche en Frontend.
Tout développeur JavaScript doit savoir comment fonctionnent les closures. Faites avec ⌐■_■.
Que diriez-vous d’un défi ? 7 Questions d’entretien sur les fermetures JavaScript. Pouvez-vous y répondre ?