Ich wurde inspiriert, diese Notiz zu schreiben, indem ich den Artikel über das Habré
"Var, let or const? Die Probleme des Umfangs der Variablen und von ES6 “ und die Kommentare dazu sowie der entsprechende Teil von
Zakas Ns Buch
„ Understanding of ECMAScript 6 “ . Aufgrund meiner Lektüre bin ich zu dem Schluss gekommen, dass nicht alles so einfach ist, um die Verwendung von
var oder
let zu beurteilen. Autoren und Kommentatoren neigen dazu zu glauben, dass es sinnvoll ist, die Verwendung von
var vollständig aufzugeben und standardmäßig einige vereinfachte Konstruktionen anstelle der alten zu verwenden, da ältere Versionen von Browsern nicht unterstützt werden müssen.
Über den Umfang dieser Anzeigen, einschließlich der oben genannten Materialien, wurde bereits genug gesagt, daher möchte ich mich nur auf einige nicht offensichtliche Punkte konzentrieren.
Zunächst möchte ich
Ausdrücke von sofort aufgerufenen Funktionen (sofort aufgerufene Funktionsausdrücke, IIFE) in Schleifen betrachten.
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); } func1.forEach(function(func) { func(); });
oder Sie können mit
let auf sie verzichten :
let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); });
Zakas N. behauptet, dass beide ähnlichen Beispiele, die das gleiche Ergebnis
liefern , auch genau gleich funktionieren:
"Diese Schleife funktioniert genau wie die Schleife, die var und ein IIFE verwendet hat, ist aber wohl sauberer."
was er selbst jedoch etwas weiter indirekt widerlegt.
Tatsache ist, dass jede Iteration der Schleife bei Verwendung von
let eine separate lokale Variable
i erstellt , während die Bindung in den an das Array gesendeten Funktionen auch zu separaten Variablen von jeder Iteration geht.
In diesem speziellen Fall ist das Ergebnis wirklich nicht anders, aber was ist, wenn wir den Code etwas komplizieren?
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(); });
Beim Hinzufügen von
++ i erwies sich unser Ergebnis als ziemlich vorhersehbar, da wir die Funktion mit
i- Werten aufgerufen haben, die zum Zeitpunkt des Aufrufs relevant waren, selbst wenn die Schleife selbst übergeben wurde. Daher hatte die nachfolgende Operation
++ i keinen Einfluss auf den Wert, der an die Funktion im Array übergeben wurde, da dies bereits der Fall war wurde in
Funktion (i) mit einem bestimmten Wert von
i geschlossen .
Vergleichen Sie nun mit der
Let- Version ohne
IIFE let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); });
Das Ergebnis hat sich anscheinend geändert, und die Art dieser Änderung ist, dass wir die Funktion nicht sofort mit dem Wert aufgerufen haben, sondern dass die Funktion die in den Abschlüssen verfügbaren Werte bei bestimmten Iterationen des Zyklus übernommen hat.
Betrachten Sie Beispiele mit zwei Arrays, um die Essenz des Geschehens besser zu verstehen. Und für den Anfang nehmen wir var ohne
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(); });
Bisher ist alles offensichtlich - es gibt keine Schließung (obwohl wir sagen können, dass dies der Fall ist, aber für den globalen Bereich, obwohl dies nicht ganz richtig ist, da der Zugang zu
i im Wesentlichen überall ist), d. H. Ähnlich, aber mit einem lokalen Bereich anscheinend wird die Variable
i einen ähnlichen Eintrag haben:
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(); });
In beiden Beispielen tritt Folgendes auf:
1. Zu Beginn der letzten Iteration des Zyklus
i == 2 , dann inkrementiert um
1 (++ i) , und am Ende wird
1 weitere aus
i ++ hinzugefügt. Infolgedessen wird am Ende des gesamten Zyklus
i == 4 .
2. Die Funktionen in den Arrays
func1 und
func2 werden
einzeln aufgerufen , und in jedem von ihnen wird dieselbe Variable
i nacheinander inkrementiert, was sich relativ zu ihrem Umfang
abschließt. Dies macht sich insbesondere dann bemerkbar, wenn es sich nicht um eine globale, sondern um eine lokale Variable handelt.
IIFE hinzufügen .
Die erste 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(); });
Die zweite 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(); });
Beim Hinzufügen von
IIFE im ersten Fall haben wir einfach die festen Werte von
i in
Funktion (i) (
0 und
2 während des ersten bzw. zweiten Durchlaufs des Zyklus) aufgerufen und um 1 erhöht, wobei jede Funktion von der anderen getrennt ist, da hier der Abschluss einer gemeinsamen Variablen erfolgt Es gibt keine Schleife, da der Wert
i sofort während der Durchläufe der Schleife übertragen wurde. Im zweiten Fall gibt es auch keinen Abschluss für die Schleifenvariable, aber dort wurde der Wert mit gleichzeitigem Inkrement übertragen, so dass am Ende des ersten Durchgangs
i == 4 war und die Schleife nicht weiter ging. Ich mache jedoch darauf aufmerksam, dass die Verschlüsse von Variablen aus externen Funktionen in internen Funktionen für jede Funktion separat sowohl in der ersten als auch in der zweiten Variante vorhanden sind. Zum Beispiel:
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(); });
Hinweis: Selbst wenn Sie den Zyklus mit einer Funktion versehen, werden übliche Verschlüsse dies natürlich nicht tun.Betrachten Sie nun die
let- Anweisung ohne IIFE.
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(); });
Und hier haben wir wieder einen Kurzschluss zur Schleifenvariablen gebildet, und zwar nicht eine, sondern zwei und nicht getrennt, sondern gemeinsam, was angesichts des bekannten Prinzips der
eingelassenen Zyklen logisch ist.
Als Ergebnis haben wir, dass im ersten Abschluss vor dem Aufrufen der Funktionen in den Arrays der Wert
i == 1 und im zweiten
i == 3 ist . Dies sind die Werte, die die Variable
i vor
i ++ und der Schleifeniteration erhalten hat, aber nach allen Anweisungen im Schleifenblock, und sie werden für jede bestimmte Iteration geschlossen.
Dann werden die im
func1- Array befindlichen Funktionen
aufgerufen und inkrementieren die entsprechenden Variablen in beiden Abschlüssen und als Ergebnis im ersten
i == 2 und im zweiten
i == 4 .
Der nachfolgende Aufruf von
func2 erhöht sich weiter und erhält
i == 3 bzw.
5 .
Ich habe
func2 und
func1 absichtlich
so in den Block
eingefügt , dass die Unabhängigkeit von ihrem Standort deutlicher sichtbar wurde, und um die Aufmerksamkeit des Lesers auf die Tatsache zu lenken, dass Schleifenvariablen geschlossen werden.
Abschließend werde ich ein triviales Beispiel geben, das darauf abzielt, das Verständnis von Schließungen und den Umfang von
let zu verbessern :
let func1 = []; { let i = 0; func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); console.log(i);
Was haben wir insgesamt?
1. Das Aufrufen von Ausdrücken sofort aufgerufener Funktionen entspricht nicht der Verwendung iterierbarer
let- Variablen in Funktionen in Schleifen und führt in einigen Fällen zu unterschiedlichen Ergebnissen.
2. Aufgrund der Tatsache, dass bei Verwendung einer
let- Deklaration für einen Iterator in jeder Iteration eine separate lokale Variable erstellt wird, stellt sich die Frage nach der Entsorgung unnötiger Daten durch den Garbage Collector. Zu diesem Zeitpunkt wollte ich zugeben, dass ich zunächst die Aufmerksamkeit auf mich ziehen wollte, da ich vermutete, dass das Erstellen einer großen Anzahl von Variablen in großen Schleifen den Compiler verlangsamen würde. Beim Sortieren eines Testarrays mit nur
let- Variablendeklarationen zeigte sich jedoch ein Gewinn an Ausführungszeit von fast zweimal für ein Array von 100.000 Zellen:
Option mit 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);
Und die Option mit 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);
Gleichzeitig war die Ausführungszeit praktisch unabhängig vom Vorhandensein / Fehlen von Anweisungen:
mit IIFE func1.push(function(i) { return function() { return i; } }(arr[i]));
entweder
ohne IIFE func1.push(function() { return arr[i]; });
und
Funktionsaufruf for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); }
Hinweis: Ich verstehe, dass die Informationen zur Geschwindigkeit nicht neu sind, aber der Vollständigkeit halber denke ich, dass diese beiden Beispiele es wert waren, angegeben zu werden.Aus all dem können wir schließen, dass die Verwendung von
let- Deklarationen anstelle von
var in Anwendungen, die keine Abwärtskompatibilität mit früheren Standards erfordern, mehr als gerechtfertigt ist, insbesondere in Fällen mit Schleifen. Gleichzeitig lohnt es sich jedoch, sich an die Verhaltensmerkmale in Situationen mit Schließungen zu erinnern und gegebenenfalls weiterhin Ausdrücke von sofort aufgerufenen Funktionen zu verwenden.