Les fermetures sont l'un des concepts fondamentaux de JavaScript, provoquant des difficultés pour de nombreux débutants, que chaque programmeur JS devrait connaître et comprendre. Ayant une bonne compréhension des fermetures, vous pouvez écrire un code meilleur, plus efficace et plus propre. Et cela, à son tour, contribuera à votre croissance professionnelle.
Le matériel, dont nous publions la traduction aujourd'hui, est consacré à l'histoire des mécanismes internes des fermetures et à leur fonctionnement dans les programmes JavaScript.
Qu'est-ce qu'une fermeture?
Une fermeture est une fonction qui a accès à un périmètre formé par une fonction externe par rapport à elle, même après que cette fonction externe a terminé son travail. Cela signifie qu'une fermeture peut stocker des variables déclarées dans une fonction externe et des arguments qui lui sont passés. Avant de procéder, en fait, aux fermetures, nous comprendrons le concept d '«environnement lexical».
Qu'est-ce qu'un environnement lexical?
Le terme «environnement lexical» ou «environnement statique» en JavaScript fait référence à la possibilité d'accéder à des variables, des fonctions et des objets en fonction de leur emplacement physique dans le code source. Prenons un exemple:
let a = 'global'; function outer() { let b = 'outer'; function inner() { let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' } console.log(a); // 'global' console.log(b); // 'outer' inner(); } outer(); console.log(a); // 'global'
Ici, la fonction
inner()
a accès aux variables déclarées dans sa propre portée, dans la portée de la fonction
outer()
et dans la portée globale. La fonction
outer()
a accès aux variables déclarées dans sa propre portée et dans la portée globale.
La chaîne de portée du code ci-dessus ressemblera à ceci:
Global { outer { inner } }
Notez que la fonction
inner()
est entourée par l'environnement lexical de la fonction
outer()
, qui à son tour est entourée d'une portée globale. C'est pourquoi la fonction
inner()
peut accéder aux variables déclarées dans la fonction
outer()
et dans la portée globale.
Exemples pratiques de fermetures
Considérez, avant de démonter les subtilités des circuits internes, quelques exemples pratiques.
▍ Exemple n ° 1
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Ici, nous appelons la fonction
person()
, qui renvoie la fonction interne
displayName()
, et stockons cette fonction dans la variable
peter
. Lorsque, après cela, nous appelons la fonction
peter()
(la variable correspondante stocke en fait une référence à la fonction
displayName()
), le nom
Peter
est affiché dans la console.
En mĂŞme temps, il n'y a pas de variable
displayName()
dans la fonction
displayName()
, nous pouvons donc conclure que cette fonction peut en quelque sorte accéder à la variable déclarée dans la fonction externe à elle,
person()
, même après cela comment cette fonction fonctionnait. C'est peut-être parce que la fonction
displayName()
est en fait une fermeture.
▍ Exemple n ° 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
Ici, comme dans l'exemple précédent, nous stockons le lien vers la fonction interne anonyme retournée par la fonction
getCounter()
dans le
count
variables. Puisque la fonction
count()
est une fermeture, elle peut accéder à la variable
counter
de la fonction
getCount()
même après que la fonction
getCounter()
a terminé son travail.
Notez que la valeur de la variable
counter
n'est pas réinitialisée à 0 à chaque appel de la fonction
count()
. Il peut sembler qu'il devrait être réinitialisé à 0, comme ce serait le cas lors de l'appel d'une fonction régulière, mais cela ne se produit pas.
Cela fonctionne exactement comme ça car chaque fois que la fonction
count()
est appelée, une nouvelle étendue est créée pour elle, mais il n'y a qu'une seule étendue pour la fonction
getCounter()
. Étant donné que la variable
counter
est déclarée dans la portée de la fonction
getCounter()
, sa valeur entre les appels Ă la fonction
count()
est enregistrée sans réinitialiser à 0.
Comment fonctionnent les courts-circuits?
Jusqu'à présent, nous avons parlé des fermetures et examiné des exemples pratiques. Parlons maintenant des mécanismes internes JavaScript qui les font fonctionner.
Afin de comprendre les fermetures, nous devons traiter avec deux concepts JavaScript cruciaux. Il s'agit du contexte d'exécution et de l'environnement lexical.
▍ Contexte d'exécution
Le contexte d'exécution est un environnement abstrait dans lequel le code JavaScript est calculé et exécuté. Lorsque du code global est exécuté, cela se produit dans le contexte d'exécution global. Le code de fonction est exécuté dans le contexte de l'exécution de la fonction.
À un moment donné, le code peut être exécuté dans un seul contexte d'exécution (JavaScript est un langage de programmation à thread unique). Ces processus sont gérés à l'aide de ce qu'on appelle la pile d'appels.
La pile d'appels est une structure de données organisée selon le principe LIFO (Last In, First Out - Last In, First Out). Les nouveaux éléments ne peuvent être placés que sur le dessus de la pile, et seuls les éléments peuvent en être supprimés.
Le contexte d'exécution actuel sera toujours en haut de la pile, et lorsque la fonction actuelle se termine, son contexte d'exécution est extrait de la pile et le contrôle est transféré dans le contexte d'exécution, qui était situé sous le contexte de cette fonction dans la pile d'appels.
Considérez l'exemple suivant pour mieux comprendre le contexte d'exécution et la pile d'appels:
Exemple de contexte d'exécutionLorsque ce code est exécuté, le moteur JavaScript crée un contexte d'exécution global pour exécuter le code global, et lorsqu'il rencontre un appel à la fonction
first()
, crée un nouveau contexte d'exécution pour cette fonction et le place en haut de la pile.
La pile d'appels de ce code ressemble Ă ceci:
Pile d'appelsLorsque l'exécution de la
first()
fonction
first()
est terminée, son contexte d'exécution est récupéré de la pile d'appels et le contrôle est transféré au contexte d'exécution en dessous, c'est-à -dire au contexte global. Après cela, le code restant dans la portée globale sera exécuté.
â–ŤEnvironnement Lexique
Chaque fois que le moteur JS crée un contexte d'exécution pour exécuter une fonction ou un code global, il crée également un nouvel environnement lexical pour stocker les variables déclarées dans cette fonction lors de son exécution.
L'environnement lexical est une structure de données qui stocke des informations sur la correspondance des identifiants et des variables. Ici, «identificateur» est le nom d'une variable ou d'une fonction, et «variable» est une référence à un objet (cela inclut les fonctions) ou une valeur d'un type primitif.
L'environnement lexical contient deux composants:
- Un enregistrement d'environnement est l'endroit où les déclarations de variables et de fonctions sont stockées.
- Référence à l'environnement externe - un lien qui vous permet d'accéder à l'environnement lexical externe (parent). Il s'agit de la composante la plus importante à traiter pour comprendre les fermetures.
Conceptuellement, l'environnement lexical ressemble Ă ceci:
lexicalEnvironment = { environmentRecord: { <identifier> : <value>, <identifier> : <value> } outer: < Reference to the parent lexical environment> }
Jetez un œil à l'extrait de code suivant:
let a = 'Hello World!'; function first() { let b = 25; console.log('Inside first function'); } first(); console.log('Inside global execution context');
Lorsque le moteur JS crée un contexte d'exécution global pour exécuter du code global, il crée également un nouvel environnement lexical pour stocker les variables et les fonctions déclarées dans la portée globale. En conséquence, l'environnement lexical de portée mondiale ressemblera à ceci:
globalLexicalEnvironment = { environmentRecord: { a : 'Hello World!', first : < reference to function object > } outer: null }
Veuillez noter que la référence à l'environnement lexical externe (
outer
) est définie sur
null
, car la portée globale n'a pas d'environnement lexical externe.
Lorsque le moteur crée un contexte d'exécution pour la
first()
fonction
first()
, il crée également un environnement lexical pour stocker les variables déclarées dans cette fonction lors de son exécution. Par conséquent, l'environnement lexical de la fonction ressemblera à ceci:
functionLexicalEnvironment = { environmentRecord: { b : 25, } outer: <globalLexicalEnvironment> }
Le lien vers l'environnement lexical externe de la fonction est défini sur
<globalLexicalEnvironment>
, car dans le code source, le code de la fonction est dans la portée globale.
Veuillez noter que lorsque la fonction termine son travail, son contexte d'exécution est récupéré de la pile d'appels, mais son environnement lexical peut être supprimé de la mémoire, ou il peut y rester. Cela dépend si, dans d'autres environnements lexicaux, il existe des références à cet environnement lexical sous la forme de liens vers un environnement lexical externe.
Analyse détaillée d'exemples de travail avec fermetures
Maintenant que nous nous sommes armés de la connaissance du contexte d'exécution et de l'environnement lexical, nous reviendrons sur les fermetures et analyserons plus en profondeur les mêmes fragments de code que nous avons déjà examinés.
▍ Exemple n ° 1
Jetez un œil à cet extrait de code:
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Lorsque la fonction
person()
est exécutée, le moteur JS crée un nouveau contexte d'exécution et un nouvel environnement lexical pour cette fonction. En fin de travail, la fonction renvoie la fonction
displayName()
, une référence à cette fonction est écrite dans la variable
peter
.
Son environnement lexical ressemblera Ă ceci:
personLexicalEnvironment = { environmentRecord: { name : 'Peter', displayName: < displayName function reference> } outer: <globalLexicalEnvironment> }
Lorsque la fonction
person()
ferme, son contexte d'exécution est extrait de la pile. Mais son environnement lexical reste en mémoire, car il existe un lien vers lui dans l'environnement lexical de sa fonction interne
displayName()
. Par conséquent, les variables déclarées dans cet environnement lexical restent disponibles.
Lorsque la fonction
peter()
est appelée (la variable correspondante stocke une référence à la fonction
displayName()
), le moteur JS crée un nouveau contexte d'exécution et un nouvel environnement lexical pour cette fonction. Cet environnement lexical ressemblera à ceci:
displayNameLexicalEnvironment = { environmentRecord: { } outer: <personLexicalEnvironment> }
Il n'y a pas de variables dans la fonction
displayName()
, donc son enregistrement d'environnement sera vide. Lors de l'exécution de cette fonction, le moteur JS tentera de trouver la variable de
name
dans l'environnement lexical de la fonction.
Étant donné que la recherche est introuvable dans l'environnement lexical de la fonction
displayName()
, la recherche se poursuivra dans l'environnement lexical externe, c'est-Ă -dire dans l'environnement lexical de la fonction
person()
, qui est toujours en mémoire. Là , le moteur trouve la variable souhaitée et affiche sa valeur dans la console.
▍ Exemple n ° 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
L'environnement lexical de la fonction
getCounter()
ressemblera Ă ceci:
getCounterLexicalEnvironment = { environmentRecord: { counter: 0, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Cette fonction renvoie une fonction anonyme qui est affectée à la variable de
count
.
Lorsque la fonction
count()
est exécutée, son environnement lexical ressemble à ceci:
countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> }
Lors de l'exécution de cette fonction, le système recherchera la variable
counter
dans son environnement lexical. Dans ce cas, Ă nouveau, l'enregistrement d'environnement de fonction est vide, de sorte que la recherche de la variable se poursuit dans l'environnement lexical externe de la fonction.
Le moteur trouve la variable, l'affiche dans la console et incrémente la variable
counter
, qui est stockée dans l'environnement lexical de la fonction
getCounter()
.
Par conséquent, l'environnement lexical de la fonction
getCounter()
après le premier appel à la fonction
count()
ressemblera Ă ceci:
getCounterLexicalEnvironment = { environmentRecord: { counter: 1, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Chaque fois que la fonction
count()
est appelée, le moteur JavaScript crée un nouvel environnement lexical pour cette fonction et incrémente la variable
counter
, ce qui entraîne des changements dans l'environnement lexical de la fonction
getCounter()
.
Résumé
Dans cet article, nous avons parlé de ce que sont les fermetures et trié les mécanismes JavaScript sous-jacents qui les sous-tendent. Les fermetures sont l'un des concepts JavaScript fondamentaux les plus importants, et chaque développeur JS doit les comprendre. La compréhension des fermetures est l'une des étapes de l'écriture d'applications efficaces et de haute qualité.
Chers lecteurs! Si vous avez de l'expérience en développement JS, veuillez partager des exemples pratiques d'utilisation des fermetures avec des débutants.
