J'ai été inspiré pour écrire cette note en lisant l'article sur le Habré
"Var, let or const? Problèmes de la portée des variables et ES6 » et leurs commentaires, ainsi que la partie correspondante du
livre de
Zakas N.« Compréhension d'ECMAScript 6 » . Sur la base de ce que j'ai lu, je suis arrivé à la conclusion que tout n'est pas si simple pour évaluer l'utilisation de
var ou
let . Les auteurs et les commentateurs sont enclins à croire qu'en l'absence de la nécessité de prendre en charge les anciennes versions des navigateurs, il est logique d'abandonner complètement l'utilisation de
var , ainsi que d'utiliser certaines constructions simplifiées, au lieu des anciennes, par défaut.
On en a déjà assez dit sur la portée de ces publicités, y compris dans les documents ci-dessus, donc je voudrais me concentrer uniquement sur certains points non évidents.
Pour commencer, je voudrais considérer les
expressions des fonctions immédiatement appelées (Immediateely Invoked Function Expression, IIFE) dans les boucles.
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); } func1.forEach(function(func) { func(); });
ou vous pouvez vous en passer en utilisant
let :
let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); });
Zakas N. affirme que les deux exemples similaires, donnant le même résultat, fonctionnent également exactement de la même manière:
"Cette boucle fonctionne exactement comme la boucle qui utilisait var et un IIFE mais est sans doute plus propre"
ce que lui-même, un peu plus loin, réfute indirectement.
Le fait est que chaque itération de la boucle lors de l'utilisation de
let crée une variable locale séparée
i , tandis que la liaison dans les fonctions envoyées au tableau va également séparer les variables de chaque itération.
Dans ce cas particulier, le résultat n'est vraiment pas différent, mais que faire si on complique un peu le code?
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); ++i; } func1.forEach(function(func) { func(); });
Ici, en ajoutant
++ i, notre résultat s'est avéré tout à fait prévisible, car nous avons appelé la fonction avec des valeurs
i qui étaient pertinentes au moment de l'appel, même lorsque la boucle elle-même est passée, donc l'opération suivante
++ i n'a pas affecté la valeur transmise à la fonction dans le tableau, car elle déjà a été fermé dans la
fonction (i) avec une valeur spécifique de
i .
Comparez maintenant avec la version
let sans
IIFE let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); });
Le résultat a apparemment changé et la nature de ce changement est que nous n'avons pas appelé la fonction avec la valeur tout de suite, mais la fonction a pris les valeurs disponibles dans les fermetures à des itérations spécifiques du cycle.
Pour mieux comprendre l'essence de ce qui se passe, considérez des exemples avec deux tableaux. Et pour commencer, prenons var, sans
IIFE :
let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); });
Tout est évident jusqu'à présent - il n'y a pas de fermeture (bien que nous puissions dire que c'est le cas, mais à l'échelle mondiale, bien que ce ne soit pas tout à fait correct, car l'accès à
i est essentiellement partout), c'est-à-dire de la même manière, mais avec une zone locale apparemment, la variable
i aura une entrée similaire:
let func1 = [], func2 = []; function test() { for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } } test(); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); });
Dans les deux exemples, les événements suivants se produisent:
1. Au début de la dernière itération du cycle
i == 2 , puis incrémenté de
1 (++ i) , et à la fin
1 plus est ajouté à partir de
i ++ , En conséquence, à la fin de tout le cycle
i == 4 .
2. Les fonctions situées dans les tableaux
func1 et
func2 sont
appelées une par une , et dans chacune d'elles la même variable
i est incrémentée séquentiellement, ce qui est en fermeture par rapport à sa portée, ce qui est particulièrement visible lorsque nous ne traitons pas avec une variable globale, mais avec une variable locale.
Ajoutez
IIFE .
La première option:
let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); });
La deuxième option:
let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(i); } }(++i)); func1.push(function(i) { return function() { console.log(i); } }(++i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); });
Lors de l'ajout de l'
IIFE dans le premier cas, nous avons simplement appelé les valeurs fixes de
i dans la
fonction (i) (
0 et
2 , lors des première et deuxième passes du cycle, respectivement), et les avons incrémentées de 1, chaque fonction est distincte de l'autre, car voici la fermeture d'une variable commune il n'y a pas de boucle, du fait que la valeur
i a été transmise immédiatement lors des passages de la boucle. Dans le second cas, il n'y a pas non plus de fermeture de la variable de boucle, mais là la valeur a été transmise avec incrémentation simultanée, donc à la fin du premier passage
i == 4 , et la boucle n'est pas allée plus loin. Mais j'attire l'attention sur le fait que les fermetures de variables des fonctions externes dans les fonctions internes, pour chaque fonction séparément, sont toujours présentes dans les première et deuxième variantes. Par exemple:
let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); });
Remarque: même si vous encadrez le cycle avec une fonction, les fermetures courantes ne le seront naturellement pas.Examinons maintenant la déclaration
let , sans IIFE, respectivement.
let func1 = [], func2 = []; for (let i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); });
Et ici, nous avons à nouveau formé un court-circuit vers la variable de boucle, et non pas un, mais deux, et non pas séparés, mais communs, ce qui est logique, étant donné le principe bien connu des cycles de passage.
Par conséquent, nous avons cela dans la première fermeture, avant d'appeler les fonctions dans les tableaux, la valeur est
i == 1 , et dans la seconde
i == 3 . Ce sont les valeurs que la variable
i a reçues avant
i ++ et l'itération de boucle, mais après toutes les instructions du bloc de boucle, et elles sont fermées pour chaque itération spécifique.
Ensuite, les fonctions situées dans le tableau
func1 sont
appelées et elles incrémentent les variables correspondantes dans les deux fermetures et, par conséquent, dans le premier
i == 2 et dans le second
i == 4 .
L'appel suivant à
func2 incrémente davantage et obtient
i == 3 et
5, respectivement.
J'ai délibérément placé
func2 et
func1 à l'intérieur du bloc de manière à ce que l'indépendance par rapport à leur emplacement soit plus clairement visible, et pour souligner l'attention du lecteur sur le fait de fermer les variables en boucle.
Enfin, je vais vous donner un exemple trivial visant à renforcer la compréhension des fermetures et à
laisser la portée:
let func1 = []; { let i = 0; func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); console.log(i);
Qu'avons-nous au total
1. L'invocation d'expressions de fonctions immédiatement appelées n'est pas équivalente à l'utilisation de variables
let itérables dans des fonctions en boucles et, dans certains cas, conduit à des résultats différents.
2. Du fait que lors de l'utilisation d'une déclaration
let pour un itérateur, une variable locale distincte est créée à chaque itération, la question se pose de l'élimination des données inutiles par le garbage collector. À ce stade, j'admets, je voulais d'abord attirer l'attention, soupçonnant que la création d'un grand nombre de variables en grand, respectivement, des boucles ralentirait le compilateur, cependant, lors du tri d'un tableau de test en utilisant uniquement des déclarations de variables
let , cela a montré un gain de temps d'exécution de presque deux fois pour un tableau de 100 000 cellules:
Option avec var: const start = Date.now(); var arr = [], func1 = [], func2 = []; for (var i = 0; i < 100000; i++) { arr.push(Math.random()); } for (var i = 0; i < 99999; i++) { var min, minind = i; for (var j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function(i) { return function() { return i; } }(arr[i])); } func1.push(function(i) { return function() { return i; } }(arr[99999])); for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000);
Et l'option avec let: const start = Date.now(); let arr = [], func1 = [], func2 = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } for (let i = 0; i < 99999; i++) { let min, minind = i; for (let j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function() { return arr[i]; }); } func1.push(function() { return arr[99999]; }); for (let i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000);
Dans le même temps, le temps d'exécution était pratiquement indépendant de la présence / absence d'instructions:
avec IIFE func1.push(function(i) { return function() { return i; } }(arr[i]));
soit
sans IIFE func1.push(function() { return arr[i]; });
et
appel de fonction for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); }
Remarque: Je comprends que les informations sur la vitesse ne sont pas nouvelles, mais pour être complet, je pense que ces deux exemples valent la peine d'être donnés.De tout cela, nous pouvons conclure que l'utilisation de déclarations
let au lieu de
var , dans les applications qui ne nécessitent pas de compatibilité descendante avec les normes antérieures, est plus que justifiée, en particulier dans les cas avec des boucles. Mais, en même temps, il convient de se rappeler les caractéristiques du comportement dans les situations de fermeture et, si nécessaire, de continuer à utiliser des expressions de fonctions immédiatement appelées.