Closures – Funktionsabschlüsse

Mario Blog, Code, Tutorial ,
0

Closures sind nicht so schwer zu verstehen.
Closures sind etwas was die Javascript – Engine für uns macht und das ist meistens verdammt praktisch aber verwirrend am Anfang.

Folgendes Beispiel:

Wenn wir jetzt samSays() ausführen und das Ergebnis einer Variablen zuweisen
var sayIt = samSays("Hello");
dann passiert optisch erstmal nichts, dafür aber umso mehr im Hintergrund:

  • Ein Execution Context für samSays() wird auf den Stack gelegt.
  • Die Variable first bekommt „Hello“ zugewiesen und liegt dann im Execution Context von samSays().
  • Das return liefert uns die Funktion zurück und wird in sayIt gespeichert.
  • Nun ist die Ausführung der Funktion zu Ende, weswegen sie vom Execution Context fliegt.

closure_deNun fliegt zwar die Funktion nach der Ausführung vom Stack, die Variable first („Hello“) aber bleibt auf dem Stack liegen und ist intern weiterhin mit samSays() verknüpft.

Jetzt führen wir sayIt("World!") aus

  • Ein neuer Execution Context für sayIt() wird angelegt.
  • Die Variable second bekommt „World!“ zugewiesen.
  • console.log wird ausgeführt und die Engine sucht in der scope chain nach der Variablen first

…und findet sie tatsächlich, obwohl sie ja eigentlich am Execution Context von samSays hing und mit ihm hätte verschwinden sollen.

Aber tatsächlich ist die Variable nicht beim löschen des Execution Contexts mit gelöscht worden, nein sie existierte weiterhin auf dem Stack und wurde durch das ausführen der zweiten Funktion und der internen Verbindung mit samSays() mit-eingeschlossen (closed-in = closure), s.Bild. Sie blieb also auf der scope chain und konnte so gefunden werden.

closure_2_de

Aus first wurde eine sogenannte „free variable“ die ausserhalb eines Execution Contexts auf dem Stack liegen. Diese werden dann automatisch im nächsten Execution Context eingeschlossen (closed-in). Allerdings muss der nächste Execution Context eine Verbindung mit dem ersten Execution Context haben in dem die Variable erstellt wurde. In unserem Fall die umschliessende Funktion samSays(), denn die Variable first ist von diesem erstellt worden. Kein anderer Execution Context kann die Variable first erreichen bzw. sehen (scope chain!).

Folgendes Beispiel findet man sehr oft im Internet wenn man sich mit Closures beschäftigt:

Eigentlich nix dramatisches. In ein Array wird viermal dieselbe Funktion reingepackt.
Nun rufen wir einmal die äussere Funktion auf um das Array zu füllen.

var farr = makeFunctions();
Und dann rufen wir die einzelnen Funktionen im Array auf.

Was sehen wir dann auf der Konsole? Überlegt mal selbst bevor ihr weiter lest!

Beim ersten hinschauen denkt man es müsste folgendes ausgegeben werden:

0
1
2
3

Tatsächlich aber erscheint dies:

4
4
4
4

Wie kann das sein? Schauen wir uns das mal im Detail an.
Wir rufen makeFunctions() auf und weisen das Ergebnis der Variablen farr zu.
Beim Aufruf von makeFunctions() erstellen wir ein leeres Array arr[].
In der for() Schleife wird i von 0 bis 4 hochgezählt und bei i = 4 wird die for() Schleife nicht mehr ausgeführt.
Bei i gleich 0-3 wird jeweils die Funktion function(){console.log(i); } ins Array gepuscht.

Ist die for() Schleife fertig, wird das Array über return in die Variable farr gepackt. Danach verschwindet makeFunctions() wieder vom Stack.
Aufgrund der Closure bleibt aber die Variable i mit dem letzen Wert (4) auf dem Stack liegen.

Führen wir nun die einzelnen Funktionen aus, die in farr gespeichert wurden aus, so zeigen alle auf i welche zu dieser Zeit auf dem Stack den Wert 4 hat.

closure_3_de

Wie kann ich hier nun trotzdem die Zahlen 0,1,2 in der Funktion im Array speichern?
In ECMAS6 z.B. mit dem let Befehl:

Bei jedem durchlauf der for Schleife wird j an einen extra Platz im Speicher abgelegt, d.h. das j in der Funktion welche im Array gespeichert wird, auf den für sie im Moment gültigen Wert zeigt. In unserem Fall auf einen Wert von 0-2.

Das ganze kann man natürlich auch ohne ECMAS6 bewerkstelligen. Hier helfen uns IIFEs:

Bei jedem Schleifendurchlauf, bei jedem ausführen des push Befehls wird auch automatisch die Funktion ausgeführt und ihr der aktuelle Wert von i übergeben. Dadurch das die Funktion jedesmal gestartet wird bekommt jede ihren eigenen Execution Context und ihre eigene Closure mit den darin enthaltenen Variablen. Die Funktion liefert uns dann wiederum eine Funktion zurück function() {console.log(j);} welche dann im Array gespeichert wird. j referenziert hier den Wert von i bei Ausführung der Funktion, den wir dann über die scope – chain in der Closure wiederfinden.

 

 

Schreibe einen Kommentar