Lorsque vous utilisez un objet, une variable ou une fonction, vous le faites volontairement. Vous pensez: "C'est là que j'ai besoin d'une variable" et vous l'ajoutez à votre code. Les fermetures, cependant, sont autre chose. Alors que la plupart des programmeurs commencent à se renseigner sur les fermetures, ces personnes utilisent déjà les fermetures sans le savoir. Probablement la même chose vous arrive. Donc, apprendre les fermetures consiste moins à apprendre une nouvelle idée qu'à apprendre à reconnaître quelque chose que vous avez rencontré plusieurs fois auparavant. En un mot, la fermeture se produit lorsqu'une fonction accède à des variables déclarées en dehors d'elle. Par exemple, la fermeture est contenue dans ce morceau de code:
let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));
Notez que c'est
user => user.startsWith(query)
une fonction. Elle utilise une variable query
. Et cette variable est déclarée en dehors de la fonction. C'est une fermeture.
Vous pouvez sauter la lecture si vous le souhaitez. Le reste de ce matériel regarde les fermetures sous un jour différent. Au lieu de parler de ce que sont les fermetures, cette partie de l'article abordera les détails de la détection des fermetures. Ceci est similaire à la façon dont les premiers programmeurs travaillaient dans les années 1960.
Étape 1: les fonctions peuvent accéder aux variables déclarées en dehors d'elles
Pour comprendre les fermetures, vous devez être assez familier avec les variables et les fonctions. Dans cet exemple, nous déclarons une variable à l'
food
intérieur d'une fonction eat
:
function eat() {
let food = 'cheese';
console.log(food + ' is good');
}
eat(); // 'cheese is good'
Que faire si vous souhaitez pouvoir modifier la valeur d'une variable ultérieurement
food
, en dehors de la fonction eat
? Pour ce faire, nous pouvons supprimer la variable elle-même de la fonction food
et la déplacer vers un niveau supérieur:
let food = 'cheese'; //
function eat() {
console.log(food + ' is good');
}
Cela vous permet de changer la variable
food
"de l'extérieur" si nécessaire:
eat(); // 'cheese is good'
food = 'pizza';
eat(); // 'pizza is good'
food = 'sushi';
eat(); // 'sushi is good'
En d'autres termes, la variable
food
n'est plus eat
locale à la fonction . Mais la fonction eat
, malgré cela, n'a aucun problème en travaillant avec cette variable. Les fonctions peuvent accéder aux variables déclarées en dehors d'elles. Arrêtez-vous un moment et vérifiez-vous, assurez-vous que cette idée ne vous pose aucun problème. Une fois cette pensée fermement ancrée dans votre esprit, passez à la deuxième étape.
Étape 2: placer le code dans l'appel de fonction
Disons que nous avons du code:
/* */
Peu importe de quel code il s'agit. Mais disons que nous devons l'exécuter deux fois.
La première façon de faire est de simplement faire une copie du code:
/* */
/* */
Une autre façon est de mettre le code en boucle:
for (let i = 0; i < 2; i++) {
/* */
}
Et la troisième manière, qui est particulièrement intéressante pour nous aujourd'hui, est de mettre ce code dans une fonction:
function doTheThing() {
/* */
}
doTheThing();
doTheThing();
L'utilisation d'une fonction nous donne une flexibilité maximale, car elle nous permet d'appeler le code donné n'importe quel nombre de fois, à tout moment et de n'importe où dans le programme.
En fait, si nécessaire, nous pouvons nous limiter à un seul appel de la nouvelle fonction:
function doTheThing() {
/* */
}
doTheThing();
Veuillez noter que le code ci-dessus est équivalent à l'extrait de code d'origine:
/* */
En d'autres termes, si nous prenons un morceau de code et "l'enveloppons" dans une fonction, puis appelons cette fonction exactement une fois, alors nous n'influencerons en aucune façon ce que fait ce code. Il existe quelques exceptions à cette règle, auxquelles nous ne prêterons pas attention, mais, en général, nous pouvons supposer que cette règle est vraie. Pensez-y un moment, habituez-vous à cette idée.
Étape 3: détection des fermetures
Nous avons trouvé deux idées:
- Les fonctions peuvent fonctionner avec des variables déclarées en dehors d'elles.
- Si vous placez le code dans une fonction et appelez cette fonction une fois, cela n'affectera pas les résultats du code.
Parlons maintenant de ce qui se passera si ces deux idées sont combinées.
Prenons l'exemple de code que nous avons examiné dans la première étape:
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
eat();
Maintenant, mettons tout cet exemple dans une fonction que nous prévoyons d'appeler une seule fois:
function liveADay() {
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
eat();
}
liveADay();
Lisez les deux exemples de code précédents et assurez-vous qu'ils sont équivalents.
Le deuxième exemple fonctionne! Mais regardons cela de plus près. Notez que la fonction
eat
est à l'intérieur d'une fonction liveADay
. Est-ce autorisé dans JavaScript? Est-il vraiment possible d'envelopper une fonction dans une autre?
Il y a des langages dans lesquels le code ainsi structuré se révélera incorrect. Par exemple, en C, un tel code serait erroné (il n'y a pas de fermetures dans cette langue). Cela signifie que lorsque vous utilisez C, notre deuxième conclusion sera incorrecte - vous ne pouvez pas simplement prendre un morceau de code arbitraire et le «envelopper» dans une fonction. Mais il n'y a pas de telle limitation dans JavaScript.
Pensons à nouveau à ce code, en accordant une attention particulière à l'endroit où la variable est déclarée et où elle est utilisée.
food
:
function liveADay() {
let food = 'cheese'; // `food`
function eat() {
console.log(food + ' is good'); // `food`
}
eat();
}
liveADay();
Passons en revue ce code étape par étape ensemble. Tout d'abord, nous déclarons, au niveau supérieur, une fonction
liveADay
. Nous l'appelons immédiatement. Cette fonction a une variable locale food
. La fonction y est également déclarée eat
. Ensuite, la liveADay
fonction est appelée en interne eat
. Puisque la fonction eat
est à l'intérieur d'une fonction liveADay
, elle eat
"voit" toutes les variables déclarées dans liveADay
. C'est pourquoi la fonction eat
peut lire la valeur de la variable food
.
C'est ce qu'on appelle la fermeture.
On parle de l'existence d'une fermeture lorsqu'une fonction (telle que
eat
) lit ou écrit la valeur d'une variable (telle que food
), qui est déclarée en dehors de celle-ci (par exemple, dans une fonction liveADay
).
Pensez à ces mots, relisez-les. Testez-vous en trouvant de quoi nous parlons dans notre exemple de code.
Voici un exemple qui a été donné au tout début de l'article:
let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));
Il peut être plus facile de remarquer la fermeture en réécrivant cet exemple à l'aide d'une expression de fonction:
let users = ['Alice', 'Dan', 'Jessica'];
// 1. query
let query = 'A';
let user = users.filter(function(user) {
// 2.
// 3. query ( !)
return user.startsWith(query);
});
Lorsqu'une fonction accède à une variable déclarée en dehors d'elle, nous l'appelons une fermeture. Le terme lui-même est utilisé de manière assez vague. Certaines personnes appelleront la fonction imbriquée elle-même, illustrée dans l'exemple, «fermeture». D'autres peuvent faire référence à un accesseur de variable externe en l'appelant une «fermeture». En pratique, cela n'a pas d'importance.
Fantôme d'appel de fonction
Les fermetures peuvent vous sembler un concept d'une simplicité trompeuse maintenant. Mais cela ne signifie pas qu'ils manquent de caractéristiques non évidentes. Si vous réfléchissez bien au fait qu'une fonction peut lire et écrire les valeurs de variables en dehors d'elle, il s'avère que cela a des conséquences assez graves.
Par exemple, cela signifie que ces variables «vivront» tant qu'une fonction imbriquée dans une autre fonction peut être appelée.
function liveADay() {
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
// eat
setTimeout(eat, 5000);
}
liveADay();
Dans cet exemple, il
food
s'agit d'une variable locale à l'intérieur d'un appel de fonction liveADay()
. Je veux juste décider que cette variable "disparaîtra" après avoir quitté la fonction, et ne reviendra pas nous hanter comme un fantôme.
Mais dans la fonction,
liveADay
nous demandons au navigateur d'appeler la fonction eat
après cinq secondes. Et cette fonction lit la valeur de la variable food
. En conséquence, il s'avère que le moteur JavaScript doit conserver la variable food
associée à l'appel active liveADay()
jusqu'à ce que la fonction soit appelée eat
.
En ce sens, les fermetures peuvent être considérées comme des «fantômes» d'appels de fonction passés ou des «souvenirs» de tels appels. Même si l'exécution de la fonction
liveADay()
terminé depuis longtemps, les variables qui y sont déclarées doivent continuer d'exister tant que la fonction imbriquée eat
peut être appelée. Heureusement, JavaScript prend en charge ces mécanismes, nous n'avons donc pas besoin de faire quoi que ce soit de spécial dans de telles situations.
Pourquoi les «fermetures» sont-elles appelées ainsi?
Vous vous demandez peut-être pourquoi les «fermetures» sont appelées de cette façon. La raison en est principalement historique. Quiconque connaît le jargon informatique pourrait dire qu'une expression comme celle-ci
user => user.startsWith(query)
a une "liaison ouverte". En d'autres termes, il ressort clairement de cette expression ce qu'est user
(paramètre), mais vu isolément, il n'est pas clair de quoi il s'agit query
. Quand nous disons qu'il s'agit, en fait, d' query
une variable qui est déclarée en dehors de la fonction, nous «fermons» cette liaison ouverte. En d'autres termes, nous obtenons une clôture.
Les fermetures ne sont pas implémentées dans tous les langages de programmation. Par exemple, dans certains langages, comme C, les fonctions imbriquées ne peuvent pas du tout être utilisées. Par conséquent, la fonction ne peut fonctionner qu'avec ses variables locales ou avec des variables globales. Cependant, il n'y a jamais de situation dans laquelle il peut accéder aux variables locales de la fonction parent. C'est en fait une limitation très désagréable.
Il existe également des langages comme Rust qui implémentent des fermetures. Mais ils utilisent une syntaxe différente pour décrire les fermetures et les fonctions normales. En conséquence, si vous avez besoin de lire la valeur d'une variable en dehors d'une fonction, vous devez utiliser une construction spéciale en utilisant Rust. La raison en est que l'utilisation de fermetures peut nécessiter que les mécanismes internes du langage stockent des variables externes (appelées «environnement») même après la fin de l'appel de fonction. Cette charge supplémentaire sur le système est acceptable en JavaScript, mais elle peut, lorsqu'elle est utilisée dans des langages de bas niveau, entraîner des problèmes de performances.
Maintenant, j'espère que vous comprenez le concept de fermetures en JavaScript.
Avez-vous des difficultés à comprendre certains concepts JavaScript?