JavaScript est un langage très fonctionnel. Cela nous donne beaucoup de liberté. Une fonction peut être créée à tout moment, passée en argument à une autre fonction, puis appelée ultérieurement depuis un endroit de code totalement différent.
Nous savons déjà qu'une fonction peut accéder à des variables extérieures à elle (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 plus anciennes ?
Et que se passe-t-il si une fonction est transmise en argument et appelée depuis un autre emplacement du code, aura-t-elle accès aux variables externes au nouvel emplacement ?
Élargissons nos connaissances pour comprendre ces scénarios et d'autres plus complexes.
Nous parlerons ici des variables let/const
En JavaScript, il existe 3 façons de déclarer une variable : let
, const
(les plus modernes) et var
(le vestige du passé).
Dans cet article, nous utiliserons les variables let
dans des exemples.
Les variables déclarées avec const
se comportent de la même manière, cet article concerne donc également const
.
L'ancienne var
présente quelques différences notables, elles seront abordées dans l'article L'ancienne "var".
Si une variable est déclarée dans un bloc de code {...}
, elle n'est visible qu'à l'intérieur de ce bloc.
Par exemple:
{ // fait un travail avec des variables locales qui ne devraient pas être vues à l'extérieur let message = "Bonjour" ; // visible uniquement dans ce bloc alerte (message); // Bonjour } alerte (message); // Erreur : le message n'est pas défini
Nous pouvons utiliser cela pour isoler un morceau de code qui fait sa propre tâche, avec des variables qui n'appartiennent qu'à lui :
{ // afficher le message let message = "Bonjour" ; alerte (message); } { // affiche un autre message let message = "Au revoir"; alerte (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 :
// afficher le message let message = "Bonjour" ; alerte (message); // affiche un autre message let message = "Au revoir"; // Erreur : variable déjà déclarée alerte (message);
For if
, for
, while
et ainsi de suite, les variables déclarées dans {...}
ne sont également visibles qu'à l'intérieur :
si (vrai) { let phrase = "Bonjour!"; alerte(phrase); // Bonjour! } alerte(phrase); // Erreur, aucune variable de ce type !
Ici, une fois if
terminé, l' alert
ci-dessous ne verra pas la phrase
, d'où l'erreur.
C'est génial, car cela nous permet de créer des variables locales de bloc, spécifiques à une branche if
.
La même chose est vraie pour les boucles for
et while
:
pour (soit i = 0; i < 3; i++) { // la variable i n'est visible à l'intérieur que pour alerte(i); // 0, puis 1, puis 2 } alerte(i); // Erreur, aucune variable de ce type
Visuellement, let i
soit en dehors de {...}
. Mais la construction for
est ici particulière : la variable, déclarée à l'intérieur, est considérée comme faisant partie du bloc.
Une fonction est dite « imbriquée » lorsqu’elle est créée dans une autre fonction.
Il est facilement possible de le faire avec JavaScript.
Nous pouvons l'utiliser pour organiser notre code, comme ceci :
fonction sayHiBye (prénom, nom) { // fonction imbriquée d'assistance à utiliser ci-dessous fonction getFullName() { return prénom + " " + nom de famille ; } alert( "Bonjour, " + getFullName() ); alert( "Au revoir, " + getFullName() ); }
Ici, la fonction imbriquée getFullName()
est conçue pour plus de commodité. Il peut accéder aux variables externes et peut ainsi renvoyer le nom complet. Les fonctions imbriquées sont assez courantes en JavaScript.
Ce qui est bien plus intéressant, une fonction imbriquée peut être renvoyée : soit comme propriété d'un nouvel objet, soit comme résultat par elle-même. Il peut ensuite être utilisé ailleurs. Peu importe où, il a toujours accès aux mêmes variables externes.
Ci-dessous, makeCounter
crée la fonction « compteur » qui renvoie le numéro suivant à chaque invocation :
fonction makeCounter() { laissez compter = 0 ; fonction de retour() { renvoie le nombre++ ; } ; } laissez compteur = makeCounter(); alerte( compteur() ); // 0 alerte( compteur() ); // 1 alerte( compteur() ); // 2
Bien qu'elles soient simples, des variantes légèrement modifiées de ce code ont des utilisations pratiques, par exemple en tant que 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 ?
Comprendre de telles choses est excellent pour la connaissance globale de JavaScript et bénéfique pour des scénarios plus complexes. Alors allons un peu en profondeur.
Voici des dragons !
L’explication technique approfondie nous attend.
Dans la mesure où je souhaite éviter les détails linguistiques de bas niveau, 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.
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 d' environnement lexical .
L'objet Environnement lexical se compose de deux parties :
Enregistrement d'environnement – un objet qui stocke toutes les variables locales en tant que propriétés (et d'autres informations comme la valeur de this
).
Une référence à l' environnement lexical externe , celui associé au code externe.
Une « variable » n'est qu'une propriété de l'objet interne spécial, Environment Record
. « Obtenir ou modifier une variable » signifie « obtenir ou modifier une propriété de cet objet ».
Dans ce code simple et sans fonctions, il n'y a qu'un seul Environnement Lexical :
Il s'agit de ce que l'on appelle l'environnement lexical global , associé à l'ensemble de l'écriture.
Sur l'image ci-dessus, le rectangle signifie Environment Record (magasin de variables) 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
.
Au fur et à mesure que le code commence à s'exécuter et continue, l'environnement lexical change.
Voici un code un peu plus long :
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, ils sont dans 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.
let phrase
apparaître. Il n'y a pas encore d'affectation, donc sa valeur est undefined
. Nous pouvons utiliser la variable à partir de maintenant.
phrase
se voit attribuer une valeur.
phrase
change la valeur.
Tout semble simple pour l’instant, non ?
Une variable est une propriété d'un objet interne spécial, associé au bloc/fonction/script en cours d'exécution.
Travailler avec des variables, c'est en fait travailler avec les propriétés de cet objet.
L'environnement lexical est un objet de spécification
« Environnement lexical » est un objet de spécification : il n'existe « théoriquement » dans la spécification du langage que pour décrire le fonctionnement des choses. Nous ne pouvons pas récupérer 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 astuces internes, tant que le comportement visible reste tel que décrit.
Une fonction est aussi une valeur, comme une variable.
La différence est qu'une déclaration de fonction est instantanément entièrement initialisée.
Lorsqu'un environnement lexical est créé, une déclaration de fonction 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 Function 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 déclarations de fonction, pas aux expressions de fonction dans lesquelles nous attribuons une fonction à une variable, comme let say = function(name)...
.
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 se fait sur la ligne, étiquetée par une flèche) :
Lors de l'appel de fonction, nous avons deux environnements lexicaux : l'intérieur (pour l'appel de 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 du name
est "John"
.
L’environnement lexical externe est l’environnement lexical global. Il contient la phrase
variable et la fonction elle-même.
L'environnement lexical interne a une référence à l'environnement lexical outer
.
Lorsque le code veut accéder à une variable, l'environnement lexical interne est recherché en premier, puis l'environnement externe, puis l'environnement plus externe et ainsi de suite jusqu'à l'environnement global.
Si une variable n'est trouvée nulle part, c'est une erreur en mode strict (sans use strict
, une affectation à une variable inexistante crée une nouvelle variable globale, pour des raisons de compatibilité avec l'ancien code).
Dans cet exemple, la recherche se déroule comme suit :
Pour la variable name
, l' alert
à l'intérieur say
la trouve immédiatement dans l'environnement lexical interne.
Lorsqu'il veut accéder phrase
, alors il n'y a pas phrase
localement, il suit donc la référence à l'environnement lexical externe et l'y trouve.
Revenons à l'exemple makeCounter
.
fonction makeCounter() { laissez compter = 0 ; fonction de retour() { renvoie le nombre++ ; } ; } laissez compteur = makeCounter();
Au début de chaque appel makeCounter()
, un nouvel objet Lexical Environment est créé pour stocker les variables de cette exécution makeCounter
.
Nous avons donc deux environnements lexicaux imbriqués, tout comme dans l'exemple ci-dessus :
Ce qui est différent, c'est que, lors de l'exécution de makeCounter()
, une petite 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 mémorisent 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 conserve la référence à l'environnement lexical où la fonction a été créée :
Ainsi, counter.[[Environment]]
a la référence à {count: 0}
Lexical Environment. C'est ainsi que la fonction se souvient de l'endroit où elle a été créée, peu importe où elle est appelée. La référence [[Environment]]
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 d'environnement lexical externe est extraite de counter.[[Environment]]
:
Désormais, lorsque le code à l'intérieur counter()
recherche une 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 le trouve et le modifie. .
Une variable est mise à jour dans l'environnement lexical où elle réside.
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.
Fermeture
Il existe un terme général de programmation « fermeture » que les développeurs devraient généralement connaître.
Une fermeture est une fonction qui mémorise ses variables externes et peut y accéder. Dans certains langages, cela n'est pas possible, ou une fonction doit être écrite d'une manière spéciale pour que cela se produise. Mais comme expliqué ci-dessus, en JavaScript, toutes les fonctions sont naturellement des fermetures (il n'y a qu'une seule exception, qui sera abordée dans la syntaxe "nouvelle fonction").
Autrement dit : ils se souviennent automatiquement de l'endroit où ils ont été créés à l'aide d'une propriété [[Environment]]
cachée, puis leur code peut accéder aux variables externes.
Lors d'un entretien, un développeur front-end reçoit une question sur « qu'est-ce qu'une fermeture ? », une réponse valide serait une définition de la fermeture et une explication selon laquelle toutes les fonctions en JavaScript sont des fermetures, et peut-être quelques mots supplémentaires sur les détails techniques : la propriété [[Environment]]
et le fonctionnement des environnements lexicaux.
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 aucune référence à cela. Comme tout objet JavaScript, il n'est conservé en mémoire que lorsqu'il est accessible.
Cependant, s'il existe une fonction imbriquée qui est toujours accessible après la fin d'une fonction, elle possède alors 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 l'achèvement de la fonction, il reste donc actif.
Par exemple:
fonction f() { soit valeur = 123 ; fonction de retour() { alerte (valeur); } } soit g = f(); // g.[[Environnement]] 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 enregistrées, alors tous les objets d'environnement lexical correspondants seront également conservés en mémoire. Dans le code ci-dessous, tous les 3 :
fonction f() { let value = Math.random(); return function() { alerte(valeur); } ; } // 3 fonctions dans un tableau, chacune d'elles est liée à l'environnement lexical // à partir de l'exécution f() correspondante soit 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 tant qu’il existe au moins une fonction imbriquée qui le référence.
Dans le code ci-dessous, une fois la fonction imbriquée supprimée, son environnement lexical englobant (et donc la value
) est nettoyé de la mémoire :
fonction f() { soit valeur = 123 ; fonction de retour() { alerte (valeur); } } soit g = f(); // tant que la fonction g existe, la valeur reste en mémoire g = nul ; // ...et maintenant la mémoire est nettoyée
Comme nous l'avons vu, en théorie, tant qu'une fonction est active, toutes les variables externes sont également conservées.
Mais en pratique, les moteurs JavaScript tentent d’optimiser cela. Ils analysent l'utilisation des variables et s'il ressort clairement du code qu'une variable externe n'est pas utilisée, elle est supprimée.
Un effet secondaire important dans la V8 (Chrome, Edge, Opera) est que cette variable deviendra indisponible lors du débogage.
Essayez d'exécuter l'exemple ci-dessous dans Chrome avec les outils de développement ouverts.
Lorsqu'il fait une pause, dans la console, tapez alert(value)
.
fonction f() { let value = Math.random(); fonction g() { débogueur ; // dans la console : tapez alert(value); Aucune variable de ce type ! } retourner g ; } soit g = f(); g();
Comme vous avez pu le constater, une telle variable n’existe pas ! En théorie, il devrait être accessible, mais le moteur l'a optimisé.
Cela peut conduire à des problèmes de débogage amusants (voire longs). L'un d'eux – nous pouvons voir une variable externe du même nom au lieu de celle attendue :
let value = "Surprise!"; fonction f() { let value = "la valeur la plus proche" ; fonction g() { débogueur ; // dans la console : tapez alert(value); Surprendre! } retourner g ; } soit g = f(); g();
Cette fonctionnalité du V8 est bonne à connaître. Si vous déboguez avec Chrome/Edge/Opera, vous le rencontrerez tôt ou tard.
Ce n'est pas un bug du débogueur, mais plutôt une particularité de la V8. Peut-être que cela changera un jour. Vous pouvez toujours le vérifier en exécutant les exemples sur cette page.
importance : 5
La fonction sayHi utilise un nom de variable externe. Lorsque la fonction s'exécute, quelle valeur va-t-elle utiliser ?
laissez nom = "Jean" ; fonction direSalut() { alert("Bonjour, " + nom); } nom = "Pete" ; disBonjour(); // qu'est-ce que cela affichera : "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 utilisateur ou une requête réseau.
La question est donc : prend-il en compte les dernières modifications ?
La réponse est : Pete .
Une fonction obtient les variables externes telles qu'elles sont actuellement, 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 celui 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-il accès aux variables externes depuis son lieu de création, ou son lieu d'invocation, ou les deux ?
fonction makeWorker() { laissez nom = "Pete" ; fonction de retour() { alerte(nom); } ; } laissez nom = "Jean" ; // crée une fonction laisser travailler = makeWorker(); // appelle-le travail(); // que va-t-il montrer ?
Quelle valeur affichera-t-il ? « Pete » ou « John » ?
La réponse est : Pete .
La fonction work()
dans le code ci-dessous obtient name
de son lieu d'origine via la référence de l'environnement lexical externe :
Le résultat est donc "Pete"
ici.
Mais s'il n'y avait pas 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 créons deux compteurs : counter
et counter2
en utilisant la même fonction makeCounter
.
Sont-ils indépendants ? Que va afficher le deuxième compteur ? 0,1
ou 2,3
ou autre chose ?
fonction makeCounter() { laissez compter = 0 ; fonction de retour() { renvoie le nombre++ ; } ; } laissez compteur = makeCounter(); laissez counter2 = makeCounter(); alerte( compteur() ); // 0 alerte( compteur() ); // 1 alerte( compteur2() ); // ? alerte( compteur2() ); // ?
La réponse : 0,1.
Les fonctions counter
et counter2
sont créées par différentes invocations de makeCounter
.
Ils 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 ça marchera ? Que va-t-il montrer ?
fonction Compteur() { laissez compter = 0 ; this.up = fonction() { retourner ++compte ; } ; this.down = fonction() { return --count; } ; } laissez compteur = 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
:
fonction Compteur() { laissez compter = 0 ; this.up = fonction() { retourner ++compte ; } ; this.down = fonction() { return --count; } ; } laissez compteur = new Counter(); alert( counter.up() ); // 1 alert( counter.up() ); // 2 alert( counter.down() ); // 1
importance : 5
Regardez le code. Quel sera le résultat de l’appel sur la dernière ligne ?
let phrase = "Bonjour" ; si (vrai) { laissez l'utilisateur = "Jean" ; fonction direSalut() { alerte(`${phrase}, ${utilisateur}`); } } disBonjour();
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 sayHi
à l'extérieur.
importance : 4
Écrivez la fonction sum
qui fonctionne comme ceci : sum(a)(b) = a+b
.
Oui, exactement de cette façon, en utilisant des doubles parenthèses (pas une faute de frappe).
Par exemple:
somme(1)(2) = 3 somme(5)(-1) = 4
Pour que les secondes parenthèses fonctionnent, les premières doivent renvoyer une fonction.
Comme ça:
fonction somme(a) { fonction de retour (b) { renvoyer a + b ; // prend "a" de l'environnement lexical externe } ; } alerte( somme(1)(2) ); // 3 alerte( somme(5)(-1) ); // 4
importance : 4
Quel sera le résultat de ce code ?
soit x = 1 ; fonction fonction() { console.log(x); // ? soit x = 2 ; } fonction();
PS Il y a un piège dans cette tâche. La solution n'est pas évidente.
Le résultat est : erreur .
Essayez de l'exécuter :
soit x = 1 ; fonction fonction() { console.log(x); // ReferenceError : Impossible d'accéder à 'x' avant l'initialisation soit x = 2 ; } fonction();
Dans cet exemple, nous pouvons observer la différence particulière entre une variable « inexistante » et « non initialisée ».
Comme vous l'avez peut-être lu dans l'article Portée des variables, fermeture, 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 il reste non initialisé jusqu'à l'instruction 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.
fonction fonction() { // la variable locale x est connue du moteur dès le début de la fonction, // mais "non initialisé" (inutilisable) jusqu'à ce que let ("zone morte") // d'où l'erreur console.log(x); // ReferenceError : Impossible d'accéder à 'x' avant l'initialisation soit x = 2 ; }
Cette zone d'inutilisabilité temporaire d'une variable (du début du bloc de code jusqu'à let
) est parfois appelée « zone morte ».
importance : 5
Nous avons une méthode intégrée arr.filter(f)
pour les tableaux. Il filtre tous les éléments via la fonction f
. S'il renvoie true
, alors 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 (inclus).
inArray([...])
– dans le tableau donné.
L'utilisation doit être comme ceci :
arr.filter(inBetween(3,6))
– sélectionne uniquement les valeurs comprises 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 */ soit arr = [1, 2, 3, 4, 5, 6, 7] ; alerte( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
Ouvrez un bac à sable avec des tests.
fonction entre(a, b) { fonction de retour (x) { retourner x >= a && x <= b; } ; } soit arr = [1, 2, 3, 4, 5, 6, 7] ; alerte( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
fonction inArray(arr) { fonction de retour (x) { return arr.includes(x); } ; } soit arr = [1, 2, 3, 4, 5, 6, 7] ; alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
Ouvrez la solution avec des tests dans un bac à sable.
importance : 5
Nous avons un tableau d'objets à trier :
laisser les utilisateurs = [ { nom : "John", âge : 20 ans, nom de famille : "Johnson" }, { nom : "Pete", âge : 18, nom de famille : "Peterson" }, { nom : "Ann", âge : 19 ans, nom de famille : "Hathaway" } ];
La manière habituelle de procéder serait la suivante :
// par nom (Ann, John, Pete) utilisateurs.sort((a, b) => a.name > b.name ? 1 : -1); // par âge (Pete, Ann, John) utilisateurs.sort((a, b) => a.age > b.age ? 1 : -1);
Pouvons-nous le rendre encore moins verbeux, comme ceci ?
utilisateurs.sort(byField('name')); utilisateurs.sort(byField('age'));
Ainsi, au lieu d’écrire une fonction, mettez simplement byField(fieldName)
.
Écrivez la fonction byField
qui peut être utilisée pour cela.
Ouvrez un bac à sable avec des tests.
fonction parField(fieldName){ return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1 ; }
Ouvrez la solution avec des tests dans un bac à sable.
importance : 5
Le code suivant crée un tableau de shooters
.
Chaque fonction est censée afficher son numéro. Mais quelque chose ne va pas…
fonction makeArmy() { laissez les tireurs = []; soit je = 0 ; tandis que (i < 10) { let shooter = function() { // crée une fonction shooter, alerte( je ); // qui devrait afficher son numéro } ; shooters.push(tireur); // et l'ajoute au tableau je++; } // ...et renvoie le tableau des tireurs renvoyer les tireurs ; } laissez armée = makeArmy(); // tous les tireurs affichent 10 au lieu de leurs chiffres 0, 1, 2, 3... armée[0](); // 10 du tireur numéro 0 armée[1](); // 10 du tireur numéro 1 armée[2](); // 10 ...et ainsi de suite.
Pourquoi tous les tireurs affichent-ils la même valeur ?
Corrigez le code pour qu'il fonctionne comme prévu.
Ouvrez un bac à sable avec des tests.
Examinons ce qui se passe exactement à l'intérieur makeArmy
et la solution deviendra évidente.
Il crée un tableau vide shooters
:
laissez les tireurs = [];
Le remplit de fonctions via shooters.push(function)
dans la boucle.
Chaque élément est une fonction, donc le tableau résultant ressemble à ceci :
tireurs = [ fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); }, fonction () { alerte(i); } ];
Le tableau est renvoyé par la fonction.
Puis, plus tard, l'appel à n'importe quel membre, par exemple army[5]()
obtiendra 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 shooter
. Lorsqu'une telle fonction est appelée, elle extrait i
de son environnement lexical externe.
Alors, quelle sera la valeur de i
?
Si l'on regarde la source :
fonction makeArmy() { ... soit je = 0 ; tandis que (i < 10) { let shooter = function() { // fonction de tir alerte( je ); // devrait afficher son numéro } ; shooters.push(tireur); // ajoute une fonction au tableau je++; } ... }
Nous pouvons voir que toutes les fonctions shooter
sont créées dans l'environnement lexical de la fonction makeArmy()
. Mais lorsque 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, 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 du bloc while {...}
, comme ceci :
fonction makeArmy() { laissez les tireurs = []; soit je = 0 ; tandis que (i < 10) { soit j = je; let shooter = function() { // fonction de tir alerte( j ); // devrait afficher son numéro } ; shooters.push(tireur); je++; } renvoyer les tireurs ; } laissez armée = makeArmy(); // Maintenant le code fonctionne correctement armée[0](); // 0 armée[5](); // 5
Ici, let j = i
déclare une variable « itération-locale » j
et copie i
dedans. Les primitives sont copiées « par valeur », nous obtenons donc en fait une copie indépendante de i
, appartenant à l'itération de boucle actuelle.
Les tireurs fonctionnent correctement, car la valeur de i
se rapproche désormais un peu plus. Pas dans l'environnement lexical makeArmy()
, mais dans l'environnement lexical qui correspond à l'itération de boucle actuelle :
Un tel problème pourrait également être évité si nous for
au début, comme ceci :
fonction makeArmy() { laissez les tireurs = []; pour(soit i = 0; i < 10; i++) { let shooter = function() { // fonction de tir alerte( je ); // devrait afficher son numéro } ; shooters.push(tireur); } renvoyer les tireurs ; } laissez armée = makeArmy(); armée[0](); // 0 armée[5](); // 5
C'est essentiellement la même chose, car for
chaque itération génère un nouvel environnement lexical, avec sa propre variable i
. Ainsi, shooter
généré à chaque itération fait référence à son propre i
, à partir de cette même itération.
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 – est-ce que cela en valait la peine ?
Eh bien, si vous pouviez facilement répondre à la question, vous ne liriez pas la solution. J’espère donc que cette tâche vous a aidé à mieux comprendre les choses.
Par ailleurs, il existe effectivement 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 un bac à sable.