In diesem Artikel werde ich versuchen, den Mechanismus zum Implementieren von Schließungen in JavaScript im Detail zu analysieren. Dafür werde ich den Chrome-Browser verwenden.
Beginnen wir mit der Definition:
Verschlüsse sind Funktionen, die auf unabhängige (freie) Variablen verweisen. Mit anderen Worten, die im Abschluss definierte Funktion "erinnert" sich an die Umgebung, in der sie erstellt wurde.
MDNWenn Ihnen in dieser Definition etwas nicht klar ist, ist es nicht beängstigend. Lesen Sie einfach weiter.
Ich bin zutiefst davon überzeugt, dass das Verstehen von etwas anhand konkreter Beispiele einfacher und schneller ist.
Daher schlage ich vor, ein Stück Code zu nehmen und mit dem Interpreter von Anfang bis Ende schrittweise zu gehen und zu sortieren, was gerade passiert.
Also fangen wir an:
Abbildung 1Wir befinden uns im globalen Kontext des Aufrufs, es ist global (im Browser auch als Fenster bezeichnet) und wir sehen, dass die Hauptfunktion bereits im aktuellen Kontext liegt und bereit ist zu arbeiten.
Abbildung 2Dies geschieht, weil alle Funktionsdeklarationen (im Folgenden als FD bezeichnet) immer in einem beliebigen Kontext angezeigt werden, sofort initialisiert werden und betriebsbereit sind. Dasselbe passiert mit Variablen, die über var deklariert wurden. Nur ihre Werte werden als undefiniert initialisiert.
Es ist auch wichtig zu verstehen, dass JavaScript auch die über let und const deklarierten Variablen "auslöst". Der einzige Unterschied besteht darin, dass sie nicht als var oder als FD initialisiert werden. Wenn wir daher vor der Initialisierung versuchen, auf sie zuzugreifen, wird ein Referenzfehler angezeigt.
Außerdem sehen wir in main eine intern versteckte Eigenschaft
[[Scopes]] - dies ist eine Liste externer Kontexte, auf die main Zugriff hat. In unserem Fall ist Global da, da main in einem globalen Kontext gestartet wird.
Die Tatsache, dass in JavaScript die Initialisierung von Verweisen auf die externe Umgebung zum Zeitpunkt der Erstellung der Funktion und nicht zum Zeitpunkt der Ausführung erfolgt, legt nahe, dass JS eine Sprache mit einem statischen Bereich ist. Und das ist wichtig.
Gehen Sie voran:
Abbildung 3Wir gehen auf die Hauptfunktion ein und das erste, was auffällt, ist das lokale Objekt (in der Spezifikation - localEnv). Dort sehen wir
a , da diese Variable über
var deklariert ist und "aufgetaucht" ist, und traditionell sehen wir alle 3 FDs (foo, bar, baz). Lassen Sie uns nun herausfinden, woher das alles kam.
Wenn ein Kontext
gestartet wird, wird die abstrakte Operation
NewDeclarativeEnvironment gestartet , mit der Sie
LexicalEnvironment (im Folgenden LE) und
VariableEnvironment initialisieren
können . Außerdem verwendet
NewDeclarativeEnvironment ein Argument - die externe LE, um die
Bereiche zu erstellen, über die wir oben gesprochen haben. LE ist eine API, mit der wir die Beziehung zwischen Bezeichnern und einzelnen Variablen und Funktionen definieren können. LE besteht aus 2 Komponenten:
- Datensatzumgebung - Ein Umgebungsdatensatz, mit dem Sie die Beziehung zwischen Bezeichnern und dem, was uns im aktuellen Anrufkontext zur Verfügung steht, bestimmen können
- Link zu externem LE. Jede Funktion verfügt beim Erstellen über eine interne Eigenschaft [[Bereiche]].
VariableEnvironment - meistens ist es dasselbe wie LE. Der Unterschied zwischen beiden besteht darin, dass sich der Wert von VariableEnvironment nie ändert und sich LE während der Codeausführung ändern kann. Um das weitere Verständnis zu vereinfachen, schlage ich vor, diese Komponenten zu einer LE zu kombinieren.
Auch im aktuellen Local gibt es dies aufgrund der Tatsache, dass
ThisBinding aufgerufen wurde - dies ist auch eine abstrakte Methode, die dies im aktuellen Kontext initialisiert.
Natürlich erhielt jeder FD sofort [[Scopes]]:
Abbildung 4Wir sehen, dass alle FDs in [[Scopes]] ein Array von [Closure main, Global] empfangen haben, was logisch ist.
Auch in der Abbildung sehen wir
Call Stack - dies ist eine Datenstruktur, die nach dem Prinzip von LIFO arbeitet - last in first out. Da JavaScript Single-Threaded ist, kann jeweils nur ein Kontext ausgeführt werden. In unserem Fall ist dies der Kontext der Hauptfunktion. Jeder neue Funktionsaufruf erstellt einen neuen Kontext, der gestapelt wird.
Am oberen Rand des Stapels befindet sich immer der aktuelle Ausführungskontext. Nachdem die Funktion ihre Ausführung abgeschlossen und der Interpreter sie beendet hat, wird der Aufrufkontext vom Stapel entfernt. Das ist alles, was wir in diesem Artikel über Call Stack wissen müssen :)
Wir fassen zusammen, was im aktuellen Kontext passiert ist:
- Zum Zeitpunkt der Erstellung erhielt main [[Scopes]] mit Links zur externen Umgebung
- Der Interpreter hat den Hauptteil der Hauptfunktion eingegeben
- Call Stack hat den Ausführungskontext main erhalten
- Dies wurde initialisiert
- LE initialisiert
In der Tat ist der schwierigste Teil vorbei. Wir fahren mit dem nächsten Schritt im Code fort:
Jetzt müssen wir baz anrufen, um das Ergebnis zu erhalten.
Abbildung 5Ein neuer Baz-Aufrufkontext wurde zu Call Stack hinzugefügt. Wir sehen, dass ein neues Closure-Objekt angezeigt wurde. Hier erhalten wir, was uns von Scopes zur Verfügung steht. Also kamen wir zum Punkt. Dies ist die Schließung. Wie Sie in
Abbildung 4 sehen, steht Closure (main) in der Liste der 'Backup'-Kontexte in baz an erster Stelle. Wieder keine Magie.
Nennen wir foo:
Abbildung 6Es ist wichtig zu wissen, dass es, egal wo wir foo nennen, immer den undefinierten Bezeichnern in seiner [[Scopes]] - Kette folgt. Nämlich in main und dann in Global, wenn nicht in main gefunden.
Nachdem sie foo ausgeführt hatte, gab sie den Wert zurück und ihr Kontext sprang aus Call Stack heraus.
Wir gehen zum Aufruf der Balkenfunktion über. Im Zusammenhang mit der Balkenausführung gibt es eine Variable mit demselben Namen wie die Variable in LE foo -
a . Wie Sie bereits vermutet haben, hat dies jedoch keine Auswirkungen. foo übernimmt weiterhin den Wert aus seinen [[Scopes]].
Der Ort des Anrufs wirkt sich nicht auf den Umfang aus, sondern nur auf den Ort der Erstellung
Logachyova
Abbildung 7Infolgedessen gibt baz 300 zurück und wird aus dem Call Stack geworfen. Dann passiert dasselbe mit dem Hauptkontext, unser Codefragment wird fertig ausgeführt.
Wir fassen zusammen:
- Während der Funktionserstellung wird [[Bereiche]] festgelegt . Dies ist sehr wichtig für das Verständnis von Abschlüssen, da der Interpreter diesen Links bei der Suche nach Werten sofort folgt
- Wenn diese Funktion aufgerufen wird, wird dann ein aktiver Ausführungskontext erstellt, der im Aufrufstapel abgelegt wird
- ThisBinding wird ausgeführt und für den aktuellen Kontext festgelegt
- Die LE wird initialisiert und alle Funktionsargumente, Variablen, die durch var und FD deklariert wurden, werden verfügbar. Wenn Variablen über let oder const deklariert sind, werden sie auch zu LE hinzugefügt
- Wenn der Interpreter im aktuellen Kontext keine Kennung findet, werden [[Bereiche]] für die weitere Suche verwendet, die alle nacheinander sortiert werden. Wenn der Wert gefunden wird, fällt der Link dazu in das spezielle Closure-Objekt. Gleichzeitig wird für jeden Kontext, den der aktuelle schließt, ein separater Abschluss mit den erforderlichen Variablen erstellt
- Wenn der Wert in keinem Bereich, einschließlich Global, gefunden wird, wird ein ReferenceError zurückgegeben.
Das ist alles!
Ich hoffe, dieser Artikel war hilfreich für Sie und jetzt verstehen Sie, wie der Sperrmechanismus in JavaScript funktioniert.
Tschüss :) Und bis bald. Gefällt mir und abonniere meinen Kanal :)