Verwenden von let-Deklarationen von Variablen und Features der resultierenden Abschlüsse in JavaScript

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(); }); /*    0 newECMA6add.js:4:59 1 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

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(); }); /*     0 newECMA6add.js:4:37 1 newECMA6add.js:4:37 2 newECMA6add.js:4:37 */ 

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(); }); /*    0 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

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(); }); /*    1 newECMA6add.js:4:37 3 newECMA6add.js:4:37 */ 

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(); }); /*    5 newECMA6add.js:6:37 6 newECMA6add.js:6:37 7 newECMA6add.js:5:37 8 newECMA6add.js:5:37 */ 

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(); }); /*     5 newECMA6add.js:7:41 6 newECMA6add.js:7:41 7 newECMA6add.js:6:41 8 newECMA6add.js:6:41 */ 

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(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 */ 
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(); }); /*    2 newECMA6add.js:6:56 1 newECMA6add.js:5:56 */ 

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(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 2 newECMA6add.js:6:56 4 newECMA6add.js:6:56 2 newECMA6add.js:5:56 4 newECMA6add.js:5:56 */ 
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(); }); /*    2 newECMA6add.js:6:41 4 newECMA6add.js:6:41 3 newECMA6add.js:5:41 5 newECMA6add.js:5:41 */ 

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); /* 1 newECMA6add.js:5:34 ReferenceError: i is not definednewECMA6add.js:10:1 */ 

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); // 9.847 


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); // 5.3 


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.

Source: https://habr.com/ru/post/de462971/


All Articles