18 octobre 2023

Variable scope, closure

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.

Nous parlerons ici des variables let/const

En 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 let dans les exemples.
  • Les variables, déclarées avec const, se comportent de la même manière, donc cet article concerne également const.
  • L’ancien var a 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);
}
Il y aurait une erreur sans blocs

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

Voilà des dragons !

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 :

  1. Environment Record – un objet qui stocke toutes les variables locales comme ses propriétés (et quelques autres informations comme la valeur de this).
  2. 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 :

  1. 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.
  2. Ensuite, la définition de let phrase apparaît. Il n’y a pas encore d’affectation, donc sa valeur est undefined. Nous pouvons utiliser la variable depuis ce moment.
  3. phrase se voit attribuer une valeur.
  4. phrase change 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

“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 de name est "John".
  • L’environnement lexical externe est l’environnement lexical global. Il a la variable phrase et 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 de say la trouve immédiatement dans l’environnement lexical interne.
  • Lorsqu’elle veut accéder à phrase, il n’y a pas de phrase localement, 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.

Closure

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.

Exercices

importance: 5

La fonction sayHi utilise un nom de variable externe. Lorsque la fonction s’exécute, quelle valeur va-t-elle utiliser ?

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // qu'affichera-t-elle : "John" ou "Pete" ?

De telles situations sont courantes à la fois dans le développement côté navigateur et côté serveur. Une fonction peut être programmée pour s’exécuter plus tard qu’elle n’est créée, par exemple après une action de l’utilisateur ou une demande réseau.

Donc, la question est : reprend-elle les derniers changements ?

La réponse est : Pete.

Une fonction obtient des variables externes telles qu’elles sont maintenant, elle utilise les valeurs les plus récentes.

Les anciennes valeurs de variable ne sont enregistrées nulle part. Lorsqu’une fonction veut une variable, elle prend la valeur actuelle de son propre environnement lexical ou de l’environnement externe.

importance: 5

La fonction makeWorker ci-dessous crée une autre fonction et la renvoie. Cette nouvelle fonction peut être appelée ailleurs.

Aura-t-elle accès aux variables externes depuis son lieu de création, ou depuis le lieu d’invocation, ou les deux ?

function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

// créons une fonction
let work = makeWorker();

// appelons-la
work(); // que va-t-elle afficher ?

Quelle valeur va-t-elle afficher ? “Pete” ou “John” ?

La réponse est : Pete.

La fonction work() dans le code ci-dessous obtient name du lieu de son origine via la référence d’environnement lexical externe :

Donc, le résultat est "Pete" ici.

Mais s’il n’y avait pas de let name dans makeWorker(), alors la recherche irait à l’extérieur et prendrait la variable globale comme nous pouvons le voir dans la chaîne ci-dessus. Dans ce cas, le résultat serait "John".

importance: 5

Ici, nous faisons deux compteurs : counter et counter2 en utilisant la même fonction makeCounter.

Sont-ils indépendants ? Que va montrer le deuxième compteur ? 0,1 ou 2,3 ou autre chose ?

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?
alert( counter2() ); // ?

La réponse : 0,1.

Les fonctions counter et counter2 sont créées par différentes invocations de makeCounter.

Elles ont donc des environnements lexicaux externes indépendants, chacun ayant son propre count.

importance: 5

Ici, un objet compteur est créé à l’aide de la fonction constructeur.

Est-ce que cela fonctionnera ? Que va-t-elle afficher ?

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };
  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?

Cela fonctionnera sûrement très bien.

Les deux fonctions imbriquées sont créées dans le même environnement Lexical externe. Elles partagent donc l’accès à la même variable count :

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };

  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1
importance: 5

Regardez ce code. Quel sera le résultat de l’appel à la dernière ligne ?

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();

Le résultat est une erreur.

La fonction sayHi est déclarée à l’intérieur du if, elle ne vit donc qu’à l’intérieur. Il n’y a pas de sayHi dehors.

importance: 4

Écrivez une fonction sum qui fonctionne comme ceci :sum(a)(b) = a + b.

Oui, exactement de cette façon, en utilisant des doubles parenthèses (ce n’est pas une faute de frappe).

Par exemple :

sum(1)(2) = 3
sum(5)(-1) = 4

Pour que les secondes parenthèses fonctionnent, les premières doivent renvoyer une fonction.

Comme ceci :

function sum(a) {

  return function(b) {
    return a + b; // prend "a" de l'environnement lexical externe
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4
importance: 4

Quel sera le résultat de ce code ?

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

P.S. Il y a un piège dans cette tâche. La solution n’est pas évidente.

Le résultat est : error.

Essayez de l’exécuter :

let x = 1;

function func() {
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 2;
}

func();

Dans cet exemple, nous pouvons observer la différence particulière entre une variable “non existante” et une variable “non initialisée”.

Comme vous l’avez peut-être lu dans l’article Variable scope, closure, une variable démarre à l’état “non initialisée” à partir du moment où l’exécution entre dans un bloc de code (ou une fonction). Et elle reste non initialisée jusqu’à la déclaration let correspondante.

En d’autres termes, une variable existe techniquement, mais ne peut pas être utilisée avant let.

Le code ci-dessus le démontre.

function func() {
  // la variable locale x est connue du moteur depuis le début de la fonction,
  // mais "non initialisée" (inutilisable) jusqu'à let ("zone morte")
  // d'où l'erreur

  console.log(x); // ReferenceError: Cannot access 'x' before initialization

  let x = 2;
}

Cette zone d’inutilisabilité temporaire d’une variable (du début du bloc de code jusqu’à let) est parfois appelée “dead zone” (zone morte).

importance: 5

Nous avons une méthode intégrée arr.filter(f) pour les tableaux. Elle filtre tous les éléments à travers la fonction f. S’elle renvoie true, cet élément est renvoyé dans le tableau résultant.

Créez un ensemble de filtres “prêts à l’emploi”:

  • inBetween(a, b) – entre a et b ou égal à eux (inclusivement).
  • inArray([...]) – dans le tableau donné.

L’usage doit être comme ceci :

  • arr.filter(inBetween(3,6)) – sélectionne uniquement les valeurs entre 3 et 6.
  • arr.filter(inArray([1,2,3])) – sélectionne uniquement les éléments correspondant à l’un des membres de [1,2,3].

Par exemple :

/* .. votre code pour inBetween et inArray */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

Open a sandbox with tests.

Filter inBetween

function inBetween(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

Filter inArray

function inArray(arr) {
  return function(x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

Ouvrez la solution avec des tests dans une sandbox.

importance: 5

Nous avons un tableau d’objets à trier :

let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" }
];

La manière habituelle de le faire serait :

// par nom (Ann, John, Pete)
users.sort((a, b) => a.name > b.name ? 1 : -1);

// par age (Pete, Ann, John)
users.sort((a, b) => a.age > b.age ? 1 : -1);

Peut-on le rendre encore moins verbeux, comme ceci ?

users.sort(byField('name'));
users.sort(byField('age'));

Donc, au lieu d’écrire une fonction, il suffit de mettre byField(fieldName).

Ecrivez la fonction byField qui peut être utilisée pour cela.

Open a sandbox with tests.

function byField(fieldName){
  return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
}

Ouvrez la solution avec des tests dans une sandbox.

importance: 5

Le code suivant crée un tableau de shooters.

Chaque fonction est censée sortir son numéro. Mais quelque chose ne va pas …

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // créer une fonction shooter
      alert( i ); // qui devrait afficher son numéro
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// tous les shooters affichent 10 au lieu de leurs numéros 0, 1, 2, 3 ...
army[0](); // 10 du shooter numéro 0
army[1](); // 10 du shooter numéro 1
army[2](); // 10 ...etc.

Pourquoi tous les shooters affichent-ils la même valeur ?

Corrigez le code pour qu’il fonctionne comme prévu.

Open a sandbox with tests.

Examinons exactement ce qui se fait à l’intérieur de makeArmy, et la solution deviendra évidente.

  1. Elle crée un tableau vide shooters:

    let shooters = [];
  2. Le remplit avec des fonctions dans la boucle via shooters.push(function).

    Chaque élément est une fonction, le tableau résultant ressemble à ceci :

    shooters = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. Le tableau est renvoyé par la fonction.

    Puis, plus tard, l’appel à n’importe quel membre, par ex. Army[5]() récupérera l’élément army[5] du tableau (qui est une fonction) et l’appellera.

    Maintenant, pourquoi toutes ces fonctions affichent-elles la même valeur, 10 ?

    C’est parce qu’il n’y a pas de variable locale i dans les fonctions de shooter. Lorsqu’une telle fonction est appelée, elle prend i de son environnement lexical externe.

    Alors, quelle sera la valeur de i ?

    Si nous regardons la source :

    function makeArmy() {
      ...
      let i = 0;
      while (i < 10) {
        let shooter = function() { // fonction shooter
          alert( i ); // should show its number
        };
        shooters.push(shooter); // ajoute une fonction au tableau
        i++;
      }
      ...
    }

    Nous pouvons voir que toutes les fonctions shooter sont créées dans l’environnement lexical de la fonction makeArmy(). Mais quand army[5]() est appelé, makeArmy a déjà terminé son travail, et la valeur finale de i est 10 (while s’arrête à i=10).

    En conséquence, toutes les fonctions shooter obtiennent la même valeur de l’environnement lexical externe et c’est-à-dire la dernière valeur, i=10.

    Comme vous pouvez le voir ci-dessus, à chaque itération d’un bloc while {...}, un nouvel environnement lexical est créé. Donc, pour résoudre ce problème, nous pouvons copier la valeur de i dans une variable dans le bloc while {...}, comme ceci :

    function makeArmy() {
      let shooters = [];
    
      let i = 0;
      while (i < 10) {
          let j = i;
          let shooter = function() { // fonction shooter
            alert( j ); // devrait afficher son numéro
          };
        shooters.push(shooter);
        i++;
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    // Maintenant, le code fonctionne correctement
    army[0](); // 0
    army[5](); // 5

    Ici, let j = i déclare une variable “itération-locale” j et y copie i. Les primitives sont copiées “par valeur”, donc nous obtenons en fait une copie indépendante de i, appartenant à l’itération de boucle courante.

    Les shooters fonctionnent correctement, car la valeur de i vit maintenant un peu plus près. Pas dans l’environnement lexical makeArmy(), mais dans l’environnement lexical qui correspond à l’itération de la boucle actuelle :

    Ce genre de problème pourrait également être évité si nous utilisions for au début, comme ceci :

    function makeArmy() {
    
      let shooters = [];
    
      for(let i = 0; i < 10; i++) {
        let shooter = function() { // fonction shooter
          alert( i ); // devrait afficher son numéro
        };
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    army[0](); // 0
    army[5](); // 5

    C’est essentiellement la même chose, car for génère un nouvel environnement lexical à chaque itération avec sa propre variable i. Ainsi, le shooter généré à chaque itération fait référence à son propre i, à partir de cette itération même.

    Maintenant que vous avez déployé tant d’efforts pour lire ceci, et que la recette finale est si simple – utilisez simplement for, vous vous demandez peut-être – cela en valait-il la peine ?

    Eh bien, si vous pouviez facilement répondre à la question, vous ne liriez pas la solution. Donc, j’espère que cette tâche doit vous avoir aidé à comprendre un peu mieux les choses.

En outre, il existe en effet des cas où l’on préfère while à for, et d’autres scénarios où de tels problèmes sont réels.

Ouvrez la solution avec des tests dans une sandbox.

Carte du tutoriel