JavaScript est un langage orienté vers le fonctionnel. Cela nous donne beaucoup de liberté. Une fonction peut être créée dynamiquement, passée en argument à une autre fonction et appelée ultérieurement à partir dâun code totalement différent.
Nous savons déjà quâune fonction peut accéder à des variables en dehors de celle-ci (variables externes).
Mais que se passe-t-il si les variables externes changent depuis la création dâune fonction ? La fonction obtiendra-t-elle des valeurs plus récentes ou les anciennes ?
Et si une fonction est transmise en tant que paramètre et appelée depuis un autre endroit du code, aura-t-elle accès aux variables externes au nouvel endroit ?
Développons maintenant nos connaissances pour inclure des scénarios plus complexes.
let/constEn JavaScript, il y a 3 façons de déclarer une variable : let, const (les modernes) et var (le vestige du passé).
- Dans cet article, nous utiliserons des variables
letdans les exemples. - Les variables, déclarées avec
const, se comportent de la même manière, donc cet article concerne égalementconst. - Lâancien
vara quelques différences notables, elles seront traitées dans lâarticle L'ancien "var".
Code blocks
Si une variable est déclarée à lâintérieur dâun bloc de code {...}, elle nâest visible quâà lâintérieur de ce bloc.
Par exemple :
{
// faire un travail avec des variables locales qui ne devraient pas être vues à l'extérieur
let message = "Hello"; // visible uniquement dans ce bloc
alert(message); // Hello
}
alert(message); // Error: message is not defined
Nous pouvons lâutiliser pour isoler un morceau de code qui fait sa propre tâche, avec des variables qui lui appartiennent uniquement :
{
// show message
let message = "Hello";
alert(message);
}
{
// show another message
let message = "Goodbye";
alert(message);
}
Veuillez noter que sans blocs séparés, il y aurait une erreur, si nous utilisons let avec le nom de variable existant :
// show message
let message = "Hello";
alert(message);
// show another message
let message = "Goodbye"; // Error: variable already declared
alert(message);
Pour if, for, while et ainsi de suite, les variables déclarées dans {...} ne sont également visibles quâà lâintérieur :
if (true) {
let phrase = "Hello!";
alert(phrase); // Hello!
}
alert(phrase); // Error, no such variable!
Ici, après la fin du if, lâalert ci-dessous ne verra pas phrase, dâoù lâerreur.
Câest super, car cela nous permet de créer des variables locales, spécifiques à une branche if.
La même chose vaut pour les boucles for et while :
for (let i = 0; i < 3; i++) {
// la variable i n'est visible que dans ce for
alert(i); // 0, ensuite 1, ensuite 2
}
alert(i); // Error, no such variable
Visuellement, let i est à lâextérieur de {...}. Mais la construction de for est spéciale ici : la variable déclarée à lâintérieur est considérée comme faisant partie du bloc.
Fonctions imbriquées
Une fonction est appelée âimbriquéeâ lorsquâelle est créée dans une autre fonction.
Il est possible de faire cela facilement avec JavaScript.
Nous pouvons lâutiliser pour organiser notre code, comme ceci :
function sayHiBye(firstName, lastName) {
// helper nested function to use below
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
Ici, la fonction imbriquée getFullName() est faite pour plus de commodité. Elle peut accéder aux variables externes et peut donc renvoyer le nom complet. Les fonctions imbriquées sont assez courantes dans JavaScript.
Ce qui est beaucoup plus intéressant, une fonction imbriquée peut être retournée : soit en tant que propriété dâun nouvel objet, soit en tant que résultat par elle-même. Elle peut ensuite être utilisée ailleurs. Peu importe où, elle a toujours accès aux mêmes variables externes.
Ci-dessous, makeCounter crée la fonction âcounterâ qui renvoie le nombre suivant à chaque appel :
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
Bien que simples, des variantes légèrement modifiées de ce code ont des utilisations pratiques, par exemple un générateur de nombres aléatoires pour générer des valeurs aléatoires pour des tests automatisés.
Comment cela marche-t-il ? Si nous créons plusieurs compteurs, seront-ils indépendants ? Que se passe-t-il avec les variables ici ?
La compréhension de ce genre de choses est excellente pour la connaissance globale de JavaScript et bénéfique pour les scénarios plus complexes. Allons donc un peu en profondeur.
Lexical Environment
Lâexplication technique approfondie reste à venir.
Bien que je souhaiterai éviter les détails de bas niveau du langage, toute compréhension sans eux serait manquante et incomplète, alors préparez-vous.
Pour plus de clarté, lâexplication est divisée en plusieurs étapes.
Ãtape 1. Variables
En JavaScript, chaque fonction en cours dâexécution, bloc de code {...} et le script dans son ensemble ont un objet associé interne (caché) connu sous le nom de Environnement Lexical.
Lâobjet environnement lexical se compose de deux parties :
- Environment Record â un objet qui stocke toutes les variables locales comme ses propriétés (et quelques autres informations comme la valeur de
this). - Une référence à lâenvironnement lexical externe, celui associé au code externe.
Une âvariableâ est juste une propriété de lâobjet interne spécial Environment Record. âPour obtenir ou modifier une variableâ signifie âpour obtenir ou modifier une propriété de cet objetâ.
Dans ce code simple sans fonctions, il nây a quâun seul environnement lexical :
Il sâagit de lâenvironnement Lexical dit global, associé à lâensemble du script.
Sur lâimage ci-dessus, le rectangle signifie Environment Record (stockage de variable) et la flèche signifie la référence externe. Lâenvironnement lexical global nâa pas de référence externe, câest pourquoi la flèche pointe vers null.
à mesure que le code commence à sâexécuter et se poursuit, lâenvironnement lexical change.
Voici un peu plus de code :
Les rectangles sur le côté droit montrent comment lâenvironnement lexical global change pendant lâexécution :
- Lorsque le script démarre, lâenvironnement lexical est prérempli avec toutes les variables déclarées.
- Initialement, elles sont à lâétat ânon initialiséâ. Câest un état interne spécial, cela signifie que le moteur connaît la variable, mais elle ne peut pas être référencée tant quâelle nâa pas été déclarée avec
let. Câest presque la même chose que si la variable nâexistait pas.
- Initialement, elles sont à lâétat ânon initialiséâ. Câest un état interne spécial, cela signifie que le moteur connaît la variable, mais elle ne peut pas être référencée tant quâelle nâa pas été déclarée avec
- Ensuite, la définition de
let phraseapparaît. Il nây a pas encore dâaffectation, donc sa valeur estundefined. Nous pouvons utiliser la variable depuis ce moment. phrasese voit attribuer une valeur.phrasechange de valeur.
Tout semble simple pour lâinstant, non ?
- Une variable est la propriété dâun objet interne spécial, associé au bloc/fonction/script en cours dâexécution.
- Travailler avec des variables, câest travailler avec les propriétés de cet objet.
âLâenvironnement lexicalâ est un objet de spécification : il nâexiste que âthéoriquementâ dans la spécification du langage pour décrire comment les choses fonctionnent. nous ne pouvons pas obtenir cet objet dans notre code et le manipuler directement.
Les moteurs JavaScript peuvent également lâoptimiser, supprimer les variables inutilisées pour économiser de la mémoire et effectuer dâautres opérations internes, tant que le comportement visible reste conforme à la description.
Step 2. Fonctions Declarations
Une fonction est également une valeur, comme une variable.
La différence est quâune fonction déclaration est instantanément et complètement initialisée.
Lorsquâun environnement lexical est créé, une fonction déclaration devient immédiatement une fonction prête à lâemploi (contrairement à let, qui est inutilisable jusquâà la déclaration).
Câest pourquoi nous pouvons utiliser une fonction, déclarée comme fonction déclaration, avant même la déclaration elle-même.
Par exemple, voici lâétat initial de lâenvironnement lexical global lorsque nous ajoutons une fonction :
Naturellement, ce comportement ne sâapplique quâaux fonctions déclarations, pas aux fonctions expressions où nous attribuons une fonction à une variable, telle que let say = function(name)....
Step 3. Environnement lexical intérieur et extérieur
Lorsquâune fonction sâexécute, au début de lâappel, un nouvel environnement lexical est créé automatiquement pour stocker les variables locales et les paramètres de lâappel.
Par exemple, pour say("John"), cela ressemble à ceci (lâexécution est à la ligne, marquée dâune flèche) :
Pendant lâappel de la fonction, nous avons deux environnements lexicaux : lâintérieur (pour lâappel de la fonction) et lâextérieur (global) :
- Lâenvironnement lexical interne correspond à lâexécution actuelle de
say. Il a une seule propriété :name, lâargument de la fonction. Nous avons appelésay("John"), donc la valeur denameest"John". - Lâenvironnement lexical externe est lâenvironnement lexical global. Il a la variable
phraseet la fonction elle-même.
Lâenvironnement lexical intérieur a une référence à lâenvironnement outer (extérieur).
Lorsque le code veut accéder à une variable â lâenvironnement lexical interne est recherché en premier, puis celui externe, puis le plus externe et ainsi de suite jusquâà celui global.
Si une variable nâest trouvée nulle part, câest une erreur en mode strict (sans use strict une affectation à une variable non existante crée une nouvelle variable globale, pour la compatibilité avec lâancien code).
Dans cet exemple, la recherche se déroule comme ceci :
- Pour la variable
name, lâalertà lâintérieur desayla trouve immédiatement dans lâenvironnement lexical interne. - Lorsquâelle veut accéder Ã
phrase, il nây a pas dephraselocalement, elle suit donc la référence à lâenvironnement lexical externe et la trouve là .
Step 4. Retourner une fonction
Revenons à lâexemple makeCounter.
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
Au début de chaque appel makeCounter(), un nouvel objet environnement lexical est créé, pour stocker les variables pour cette exécution makeCounter.
Nous avons donc deux environnements lexicaux imbriqués, comme dans lâexemple ci-dessus :
Ce qui est différent, câest que, pendant lâexécution de makeCounter(), une minuscule fonction imbriquée est créée à partir dâune seule ligne : return count++. Nous ne lâexécutons pas encore, nous créons seulement.
Toutes les fonctions se souviennent de lâenvironnement lexical dans lequel elles ont été créées. Techniquement, il nây a pas de magie ici : toutes les fonctions ont la propriété cachée nommée [[Environment]], qui garde la référence à lâenvironnement lexical où la fonction a été créée :
Ainsi, counter.[[Environment]] a la référence à lâenvironnement lexical {count: 0}. Câest ainsi que la fonction se souvient de lâendroit où elle a été créée, quel que soit son nom. La référence [[Environnement]] est définie une fois pour toutes au moment de la création de la fonction.
Plus tard, lorsque counter() est appelé, un nouvel environnement lexical est créé pour lâappel, et sa référence externe à lâenvironnement lexical est tirée de counter.[[Environnement]] :
Maintenant, lorsque le code à lâintérieur de counter() recherche la variable count, il recherche dâabord son propre environnement lexical (vide, car il nây a pas de variables locales), puis lâenvironnement lexical de lâappel externe makeCounter(), où il la trouve et la change.
Une variable est mise à jour dans lâenvironnement lexical où elle se trouve.
Voici lâétat après lâexécution :
Si nous appelons counter() plusieurs fois, la variable count sera augmentée à 2, 3 et ainsi de suite, au même endroit.
Il existe un terme général de programmation âclosureâ, que les développeurs devraient généralement connaître.
Une closure est une fonction qui se souvient de ses variables externes et peut y accéder. Dans certains langages, ce nâest pas possible, ou une fonction doit être écrite dâune manière spécifique pour y arriver. Mais comme expliqué ci-dessus, en JavaScript, toutes les fonctions sont naturellement des fermetures (il nây a quâune seule exception, à couvrir dans La syntaxe "new Function").
Câest-à -dire : elles se souviennent automatiquement de lâendroit où elles ont été créées en utilisant une propriété cachée [[Environnement]], puis leur code peut accéder aux variables externes.
Lors dâun entretien dâembauche, un développeur frontend reçoit assez souvent une question du genre âquâest-ce quâune closure ?â. Une réponse valide serait une définition de la closure ainsi quâune explication sur le fait que toutes les fonctions en JavaScript sont des closures, et peut-être quelques mots de plus sur les détails techniques : la propriété [[Environment]] et comment fonctionnent les environnements lexicaux.
Garbage collection
Habituellement, un environnement lexical est supprimé de la mémoire avec toutes les variables une fois lâappel de fonction terminé. Câest parce quâil nây a plus aucune référence à cela. Comme tout objet JavaScript, il nâest conservé en mémoire que lorsquâil est accessible.
Cependant, sâil y a une fonction imbriquée qui est toujours accessible après la fin dâune fonction, alors elle a la propriété [[Environment]] qui fait référence à lâenvironnement lexical.
Dans ce cas, lâenvironnement lexical est toujours accessible même après la fin de la fonction, il reste donc en vie.
Par exemple :
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g.[[Environment]] stocke une référence à l'environnement lexical
// de l'appel f() correspondant
Veuillez noter que si f() est appelé plusieurs fois et que les fonctions résultantes sont sauvegardées, tous les objets correspondants de lâenvironnement lexical seront également conservés en mémoire. Dans le code ci-dessous, ils sont tous les trois :
function f() {
let value = math.random();
return function() { alert(value); };
}
// 3 fonctions dans un tableau, chacune d'entre elles étant liée à l'environnement lexical
// à partir de l'exécution de f() correspondante
let arr = [f(), f(), f()];
Un objet environnement lexical meurt lorsquâil devient inaccessible (comme tout autre objet). En dâautres termes, il nâexiste que sâil existe au moins une fonction imbriquée qui le référence.
Dans le code ci-dessous, une fois que la fonction imbriquée est supprimée, son environnement lexical englobant (et donc la value) est nettoyé de la mémoire :
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // tant que la fonction g existe, la valeur reste en mémoire
g = null; // ⦠et maintenant la mémoire est nettoyée
Optimisations réelles
Comme nous lâavons vu, en théorie, lorsquâune fonction est vivante, toutes les variables externes sont également conservées.
Mais dans la pratique, les moteurs JavaScript tentent dâoptimiser cela. Ils analysent lâutilisation des variables et sâil est évident dâaprès le code quâune variable externe nâest pas utilisée â elle est supprimée.
Un effet secondaire important dans V8 (Chrome, Edge, Opera) est quâune telle variable ne sera plus disponible lors du débogage.
Essayez dâexécuter lâexemple ci-dessous sous Chrome avec les outils de développement ouverts.
Quand il se met en pause, dans la console, tapez alert(value).
function f() {
let value = Math.random();
function g() {
debugger; // dans la console : tapez alert(value); No such variable!
}
return g;
}
let g = f();
g();
Comme vous avez pu le constater, cette variable nâexiste pas ! En théorie, elle devrait être accessible, mais le moteur lâa optimisée.
Cela peut conduire à des problèmes de débogage amusants (voire fastidieux). Lâun dâeux â nous pouvons voir une variable externe portant le même nom au lieu de celle attendue :
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
debugger; // dans la console : tapez alert(value); Surprise!
}
return g;
}
let g = f();
g();
Cette fonctionnalité du V8 est bonne à savoir. Si vous déboguez avec Chrome/Edge/Opera, tôt ou tard vous la rencontrerez.
Ce nâest pas un bogue dans le débogueur, mais plutôt une caractéristique spéciale de V8. Peut-être que cela sera changé un jour. Vous pouvez toujours le vérifier en exécutant les exemples sur cette page.

Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)