Schließungen in Schleifen: Wie man beim Erfassen von Variablen nicht verrückt wird

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Warum verhält sich Ihr JavaScript-Code wie eine stille Post?

Stellen Sie sich vor: Sie schreiben eine einfache Schleife, fügen Event-Handler zu Schaltflächen hinzu, und plötzlich – bumm! – zeigen alle Schaltflächen dieselbe Zahl an. Kommt Ihnen das bekannt vor? Wenn Sie schon einmal mit Schließungen in JavaScript-Schleifen zu tun hatten, kennen Sie diesen Moment der kognitiven Dissonanz. Sie sind sich sicher, dass der Code korrekt ist, aber der Browser beharrt darauf: "Nein, Freundchen, da hast du etwas durcheinandergebracht."

Auf Stack Overflow hat diese Frage Hunderttausende von Aufrufen gesammelt. Und das ist kein Wunder: Das Erfassen von Schleifenvariablen in einer Schließung ist eine der häufigsten Fallen für Anfänger und sogar erfahrene Entwickler. Lassen Sie uns verstehen, warum das passiert und, was noch wichtiger ist, wie man es unter Berücksichtigung der Leistung behebt.



Das klassische Problem: var und Asynchronität

Werfen Sie einen Blick auf diesen harmlosen Code:

for (var i = 0; i < 5; i++) {    setTimeout(function() {        console.log(i); // Was wird ausgegeben?    }, 100);}

Wenn Sie "0, 1, 2, 3, 4" geantwortet haben – herzlichen Glückwunsch, Sie sind ein Optimist. Die Realität ist grausam: Die Konsole zeigt 5, 5, 5, 5, 5 an. Warum? Weil var einen funktionalen Gültigkeitsbereich hat, keinen blockbezogenen. Zu dem Zeitpunkt, an dem setTimeout ausgeführt wird, ist die Schleife bereits beendet und i ist gleich 5. Alle fünf Schließungen verweisen auf dieselbe Variable.

Dies ist ein klassisches Beispiel für das Problem der Schließungen in JavaScript-Schleifen. Und die Lösung liegt auf der Hand: Für jede Iteration muss ein neuer Gültigkeitsbereich erstellt werden.



Lösung Nr. 1: IIFE – der gute alte Workaround

Vor dem Aufkommen von ES6 verwendeten Entwickler sofort aufgerufene Funktionsausdrücke (IIFE):

for (var i = 0; i < 5; i++) {    (function(j) {        setTimeout(function() {            console.log(j);        }, 100);    })(i);}

Hier ist j eine Kopie von i zum Zeitpunkt des Aufrufs der IIFE. Jede Schließung erfasst ihren eigenen Wert. Funktioniert es? Ja. Sieht es schön aus? Nicht wirklich. Aus Sicht der Optimierung von Schließungen erzeugt dies eine zusätzliche Funktion pro Iteration, was bei einer sehr großen Schleife den Speicher belasten kann.



Lösung Nr. 2: let – der Retter aus ES6

Mit dem Aufkommen von let wurde das Problem elegant gelöst:

for (let i = 0; i < 5; i++) {    setTimeout(function() {        console.log(i); // 0, 1, 2, 3, 4    }, 100);}

let erstellt für jede Iteration der Schleife eine neue Bindung. Dies ist ein eingebautes Verhalten, das in der ECMAScript-Spezifikation verankert ist. Jede Schließung erhält ihre eigene Kopie von i. Leistung? Hervorragend – die Engine optimiert dies auf Kompilierungsebene.

Es gibt jedoch eine Nuance: Wenn Sie let innerhalb der Schleife, aber außerhalb des for-Blocks verwenden, kann das Verhalten abweichen. Halten Sie let immer im Kopf der Schleife.



Lösung Nr. 3: bind – der funktionale Ansatz

Die Methode bind ermöglicht es, Argumente festzulegen:

for (var i = 0; i < 5; i++) {    setTimeout(function(j) {        console.log(j);    }.bind(null, i), 100);}

Hier erstellt bind eine neue Funktion mit einem voreingestellten Argument. Das funktioniert, erzeugt aber eine zusätzliche Funktion pro Iteration. In Bezug auf die Leistung ist bind aufgrund interner Prüfungen etwas langsamer als IIFE.



Lösung Nr. 4: forEach und andere Array-Methoden

Manchmal ist der beste Weg, das Problem zu vermeiden, for nicht zu verwenden:

[0, 1, 2, 3, 4].forEach(function(i) {    setTimeout(function() {        console.log(i);    }, 100);});

forEach erstellt über den Callback einen neuen Gültigkeitsbereich für jede Iteration. Dies löst nicht nur

Blogs

Buchempfehlungen