Dans cet article, je vais essayer d'analyser en détail le mécanisme de mise en œuvre des fermetures en JavaScript. Pour cela, j'utiliserai le navigateur Chrome.
Commençons par la définition:
Les fermetures sont des fonctions qui font référence à des variables indépendantes (libres). En d'autres termes, la fonction définie dans la fermeture «se souvient» de l'environnement dans lequel elle a été créée.
MDNSi quelque chose n'est pas clair pour vous dans cette définition, ce n'est pas effrayant. Continuez à lire.
Je suis profondément convaincu que la compréhension de quelque chose est plus facile et plus rapide avec des exemples spécifiques.
Par conséquent, je suggère de prendre un morceau de code et de marcher avec l'interprète du début à la fin par étapes et de trier ce qui se passe.
Commençons donc:
Figure 1Nous sommes dans le contexte global de l'appel, c'est Global (aka Window dans le navigateur) et nous voyons que la fonction principale se situe déjà dans le contexte actuel et est prête à fonctionner.
Figure 2Cela se produit car toutes les déclarations de fonctions (ci-après dénommées FD) montent toujours dans n'importe quel contexte, sont immédiatement initialisées et prêtes à fonctionner. La même chose se produit avec les variables déclarées via var, seules leurs valeurs sont initialisées comme non définies.
Il est également important de comprendre que JavaScript "soulève" également les variables déclarées via let et const. La seule différence est qu'il ne les initialise pas en var ou en FD. Par conséquent, lorsque nous essayons d'y accéder avant l'initialisation, nous obtenons une erreur de référence.
De plus, dans main, nous voyons une propriété cachée en interne
[[Scopes]] - c'est une liste de contextes externes auxquels main a accès. Dans notre cas, Global est là, puisque main est lancé dans un contexte mondial.
Le fait qu'en JavaScript l'initialisation des références à l'environnement externe se produit au moment de la création de la fonction, et non au moment de l'exécution, suggère que JS est un langage avec une portée statique. Et c'est important.
Allez-y:
Figure 3Nous entrons dans la fonction principale et la première chose qui attire votre attention est l'objet Local (dans la spécification - localEnv). Là, nous voyons
un , puisque cette variable est déclarée via
var et elle est apparue, eh bien, et par tradition, nous voyons les 3 FD (foo, bar, baz). Voyons maintenant d'où tout cela vient.
Lorsqu'un contexte démarre, l'opération abstraite
NewDeclarativeEnvironment est lancée , ce qui vous permet d'initialiser
LexicalEnvironment (ci-après LE) et
VariableEnvironment . De plus,
NewDeclarativeEnvironment prend 1 argument - le LE externe, afin de créer le [[Scopes]] dont nous avons parlé ci-dessus. LE est une API qui nous permet de définir la relation entre les identifiants et les variables individuelles, les fonctions. LE se compose de 2 composants:
- Environnement d'enregistrement - un enregistrement d'environnement qui vous permet de déterminer la relation entre les identifiants et ce qui est disponible pour nous dans le contexte d'appel actuel
- Lien vers un LE externe. Chaque fonction possède une propriété [[Scopes]] interne lors de sa création .
Environnement variable - le plus souvent, c'est la même chose que LE. La différence entre les deux est que la valeur de VariableEnvironment ne change jamais et LE peut changer pendant l'exécution du code. Pour simplifier la compréhension, je propose de combiner ces composants en un seul - LE.
Dans la section locale actuelle, cela est également dû au fait que
ThisBinding a été appelé - il s'agit également d'une méthode abstraite qui initialise cela dans le contexte actuel.
Bien sûr, chaque FD a immédiatement reçu [[Scopes]]:
Figure 4Nous voyons que tous les FD ont reçu dans [[Scopes]] un tableau de [Closure main, Global], ce qui est logique.
Dans la figure également, nous voyons
Call Stack - c'est une structure de données qui fonctionne sur le principe de LIFO - dernier entré premier sorti. Étant donné que JavaScript est monothread, un seul contexte peut être exécuté à la fois. Dans notre cas, c'est le contexte de la fonction principale. Chaque nouvel appel de fonction crée un nouveau contexte, qui est empilé.
En haut de la pile se trouve toujours le contexte d'exécution actuel. Une fois que la fonction a terminé son exécution et que l'interpréteur l'a quittée, le contexte d'appel est supprimé de la pile. C'est tout ce que nous devons savoir sur Call Stack dans cet article :)
Nous résumons ce qui s'est passé dans le contexte actuel:
- Au moment de la création, principal reçu [[Scopes]] avec des liens vers l'environnement externe
- L'interprète est entré dans le corps de la fonction principale
- Call Stack a obtenu le contexte d'exécution principal
- Cela a initialisé
- LE initialisé
En fait, la partie la plus difficile est terminée. Nous passons à l'étape suivante du code:
Maintenant, nous devons appeler baz pour obtenir le résultat.
Figure 5Un nouveau contexte d'appel baz a été ajouté à la pile d'appels. Nous voyons qu'un nouvel objet Closure est apparu. Ici, nous obtenons ce qui est disponible à partir de [[Scopes]]. Nous sommes donc arrivés au point. Ceci est la fermeture. Comme vous le voyez dans la
figure 4, la fermeture (principale) apparaît en premier dans la liste des contextes de «sauvegarde» dans baz. Encore une fois pas de magie.
Appelons foo:
Figure 6Il est important de savoir que peu importe où nous appelons foo, il suivra toujours les identifiants non définis dans sa chaîne [[Scopes]]. À savoir, dans le principal puis dans Global, s'il n'est pas trouvé dans le principal.
Après avoir exécuté foo, elle a renvoyé la valeur et son contexte est sorti de Call Stack.
Nous passons à l'appel à la fonction bar. Dans le contexte de l'exécution de la barre, il existe une variable portant le même nom que la variable dans LE foo -
a . Mais, comme vous l'avez déjà deviné, cela n'affecte rien. foo prendra toujours la valeur de ses [[Scopes]].
Le lieu d'appel n'affecte pas le champ d'application, seul le lieu de création
logachyova
Figure 7En conséquence, baz renverra 300 et sera jeté hors de la pile des appels. Ensuite, la même chose se produira avec le contexte principal, notre fragment de code finira de s'exécuter.
Nous résumons:
- Lors de la création de la fonction, [[Scopes]] est défini . Ceci est très important pour comprendre les fermetures, car l'interprète suit immédiatement ces liens lors de la recherche de valeurs
- Ensuite, lorsque cette fonction est appelée, un contexte d'exécution actif est créé, qui est placé dans la pile d'appels
- ThisBinding est exécuté et défini pour le contexte actuel
- Le LE est initialisé et tous les arguments de fonction, les variables déclarées via var et FD deviennent disponibles. De plus, s'il y a des variables déclarées via let ou const, elles sont également ajoutées à LE
- Si l'interpréteur ne trouve aucun identifiant dans le contexte actuel, alors [[Scopes]] sont utilisés pour une recherche plus approfondie, qui sont triés tous tour à tour. Si la valeur est trouvée, le lien vers celle-ci tombe dans l'objet de fermeture spécial. En même temps, pour chaque contexte sur lequel se ferme le contexte actuel, une fermeture distincte est créée avec les variables nécessaires
- Si la valeur n'est trouvée dans aucune étendue, y compris Global, une ReferenceError est retournée.
C'est tout!
J'espère que cet article vous a été utile et que vous comprenez maintenant comment fonctionne le mécanisme de verrouillage en JavaScript.
Au revoir :) Et à bientôt. Aimez et abonnez-vous à ma chaîne :)