Closures sind eines der grundlegenden Konzepte von JavaScript und verursachen für viele Anfänger Schwierigkeiten, die jeder JS-Programmierer kennen und verstehen sollte. Mit einem guten Verständnis der Verschlüsse können Sie besseren, effizienteren und saubereren Code schreiben. Dies wiederum trägt zu Ihrer beruflichen Weiterentwicklung bei.
Das Material, dessen Übersetzung wir heute veröffentlichen, widmet sich der Geschichte der internen Mechanismen von Schließungen und ihrer Funktionsweise in JavaScript-Programmen.
Was ist eine Schließung?
Ein Abschluss ist eine Funktion, die Zugriff auf einen Bereich hat, der von einer externen Funktion relativ zu ihm gebildet wird, selbst nachdem diese externe Funktion ihre Arbeit abgeschlossen hat. Dies bedeutet, dass ein Abschluss Variablen speichern kann, die in einer externen Funktion deklariert sind, und an sie übergebene Argumente. Bevor wir tatsächlich zu Schließungen übergehen, werden wir uns mit dem Konzept der „lexikalischen Umgebung“ befassen.
Was ist eine lexikalische Umgebung?
Der Begriff "lexikalische Umgebung" oder "statische Umgebung" in JavaScript bezieht sich auf die Möglichkeit, auf Variablen, Funktionen und Objekte basierend auf ihrer physischen Position im Quellcode zuzugreifen. Betrachten Sie ein Beispiel:
let a = 'global'; function outer() { let b = 'outer'; function inner() { let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' } console.log(a); // 'global' console.log(b); // 'outer' inner(); } outer(); console.log(a); // 'global'
Hier hat die
inner()
Funktion Zugriff auf Variablen, die in ihrem eigenen Bereich, im Bereich der
outer()
Funktion und im globalen Bereich deklariert sind. Die Funktion
outer()
hat Zugriff auf Variablen, die in ihrem eigenen Bereich und im globalen Bereich deklariert sind.
Die Gültigkeitsbereichskette des obigen Codes sieht folgendermaßen aus:
Global { outer { inner } }
Beachten Sie, dass die Funktion
inner()
von der lexikalischen Umgebung der Funktion
outer()
ist, die wiederum von einem globalen Bereich umgeben ist. Aus diesem Grund kann die Funktion
inner()
auf Variablen zugreifen, die in der Funktion
outer()
und im globalen Bereich deklariert sind.
Praktische Beispiele für Verschlüsse
Betrachten Sie vor dem Zerlegen der Feinheiten der internen Schaltkreise einige praktische Beispiele.
▍ Beispiel Nr. 1
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Hier rufen wir die Funktion
person()
, die die interne Funktion
displayName()
, und speichern diese Funktion in der Variablen
peter
. Wenn wir danach die Funktion
peter()
aufrufen (die entsprechende Variable speichert tatsächlich einen Verweis auf die Funktion
displayName()
), wird der Name
Peter
in der Konsole angezeigt.
Gleichzeitig hat die Funktion
displayName()
keine Variable mit dem Namen
name
, sodass wir daraus schließen können, dass diese Funktion auch danach irgendwie auf die Variable zugreifen kann, die in der externen Funktion
person()
deklariert ist wie diese Funktion funktionierte. Möglicherweise liegt dies daran, dass die Funktion
displayName()
tatsächlich ein Abschluss ist.
▍ Beispiel Nr. 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
Hier speichern wir wie im vorherigen Beispiel den Link zu der anonymen internen Funktion, die von der Funktion
getCounter()
wird, in der Variablenanzahl. Da die Funktion
count()
ein Abschluss ist, kann sie auf die
getCount()
Funktion
getCounter()
auch nachdem die Funktion
getCounter()
ihre Arbeit abgeschlossen hat.
Beachten Sie, dass der Wert der
counter
nicht bei jedem Aufruf der Funktion
count()
auf 0 zurückgesetzt wird. Es scheint, dass es auf 0 zurückgesetzt werden sollte, wie es beim Aufrufen einer regulären Funktion der Fall wäre, aber dies geschieht nicht.
Dies funktioniert einfach so, da bei jedem Aufruf der Funktion
count()
ein neuer Bereich dafür erstellt wird, es jedoch nur einen Bereich für die Funktion
getCounter()
. Da die
getCounter()
im Bereich der Funktion
getCounter()
deklariert ist, wird ihr Wert zwischen Aufrufen der Funktion
count()
gespeichert, ohne auf 0 zurückgesetzt zu werden.
Wie funktionieren Kurzschlüsse?
Bisher haben wir darüber gesprochen, was Verschlüsse sind, und praktische Beispiele untersucht. Lassen Sie uns nun über die internen JavaScript-Mechanismen sprechen, mit denen sie funktionieren.
Um Abschlüsse zu verstehen, müssen wir uns mit zwei entscheidenden JavaScript-Konzepten befassen. Dies ist der Ausführungskontext und die lexikalische Umgebung.
▍ Ausführungskontext
Der Ausführungskontext ist eine abstrakte Umgebung, in der JavaScript-Code berechnet und ausgeführt wird. Wenn globaler Code ausgeführt wird, geschieht dies im globalen Ausführungskontext. Der Funktionscode wird im Kontext der Funktion ausgeführt.
Zu einem bestimmten Zeitpunkt kann Code nur in einem Ausführungskontext ausgeführt werden (JavaScript ist eine Single-Threaded-Programmiersprache). Diese Prozesse werden über den sogenannten Call Stack verwaltet.
Der Aufrufstapel ist eine Datenstruktur, die nach dem LIFO-Prinzip angeordnet ist (Last In, First Out - Last In, First Out). Neue Elemente können nur oben auf dem Stapel platziert und nur Elemente daraus entfernt werden.
Der aktuelle Ausführungskontext befindet sich immer oben im Stapel. Wenn die aktuelle Funktion beendet wird, wird ihr Ausführungskontext aus dem Stapel gezogen und die Steuerung in den Ausführungskontext übertragen, der sich im Aufrufstapel unterhalb des Kontexts dieser Funktion befand.
Betrachten Sie das folgende Beispiel, um den Ausführungskontext und den Aufrufstapel besser zu verstehen:
Beispiel für einen AusführungskontextWenn dieser Code ausgeführt wird, erstellt die JavaScript-Engine einen globalen Ausführungskontext zum Ausführen des globalen Codes. Wenn sie auf einen Aufruf der Funktion
first()
stößt, erstellt sie einen neuen Ausführungskontext für diese Funktion und platziert ihn oben auf dem Stapel.
Der Aufrufstapel dieses Codes sieht folgendermaßen aus:
Stapel aufrufenWenn die Ausführung der Funktion
first()
abgeschlossen ist, wird ihr Ausführungskontext aus dem Aufrufstapel abgerufen und die Steuerung in den darunter liegenden Ausführungskontext, dh in den globalen Kontext, übertragen. Danach wird der im globalen Bereich verbleibende Code ausgeführt.
▍Lexische Umgebung
Jedes Mal, wenn die JS-Engine einen Ausführungskontext zum Ausführen einer Funktion oder eines globalen Codes erstellt, erstellt sie auch eine neue lexikalische Umgebung zum Speichern der in dieser Funktion deklarierten Variablen während ihrer Ausführung.
Die lexikalische Umgebung ist eine Datenstruktur, in der Informationen über die Entsprechung von Bezeichnern und Variablen gespeichert werden. Hier ist "Bezeichner" der Name einer Variablen oder Funktion, und "Variable" ist eine Referenz auf ein Objekt (dies schließt Funktionen ein) oder einen Wert eines primitiven Typs.
Die lexikalische Umgebung enthält zwei Komponenten:
- Ein Umgebungsdatensatz ist der Ort, an dem Variablen- und Funktionsdeklarationen gespeichert werden.
- Verweis auf die äußere Umgebung - ein Link, über den Sie auf die externe (übergeordnete) lexikalische Umgebung zugreifen können. Dies ist die wichtigste Komponente, die behandelt werden muss, um Verschlüsse zu verstehen.
Konzeptionell sieht die lexikalische Umgebung folgendermaßen aus:
lexicalEnvironment = { environmentRecord: { <identifier> : <value>, <identifier> : <value> } outer: < Reference to the parent lexical environment> }
Schauen Sie sich das folgende Code-Snippet an:
let a = 'Hello World!'; function first() { let b = 25; console.log('Inside first function'); } first(); console.log('Inside global execution context');
Wenn die JS-Engine einen globalen Ausführungskontext zum Ausführen von globalem Code erstellt, erstellt sie auch eine neue lexikalische Umgebung zum Speichern von Variablen und Funktionen, die im globalen Bereich deklariert sind. Infolgedessen sieht die lexikalische Umgebung des globalen Bereichs folgendermaßen aus:
globalLexicalEnvironment = { environmentRecord: { a : 'Hello World!', first : < reference to function object > } outer: null }
Bitte beachten Sie, dass der Verweis auf die externe lexikalische Umgebung (
outer
) auf
null
, da der globale Bereich keine externe lexikalische Umgebung hat.
Wenn die Engine einen Ausführungskontext für die Funktion
first()
, erstellt sie auch eine lexikalische Umgebung zum Speichern der in dieser Funktion deklarierten Variablen während ihrer Ausführung. Infolgedessen sieht die lexikalische Umgebung der Funktion folgendermaßen aus:
functionLexicalEnvironment = { environmentRecord: { b : 25, } outer: <globalLexicalEnvironment> }
Die Verknüpfung zur externen lexikalischen Umgebung der Funktion wird auf
<globalLexicalEnvironment>
, da sich der Funktionscode im Quellcode im globalen Bereich befindet.
Beachten Sie, dass nach Beendigung der Arbeit der Ausführungskontext der Funktion aus dem Aufrufstapel abgerufen wird, die lexikalische Umgebung jedoch möglicherweise aus dem Speicher gelöscht wird oder dort verbleibt. Es hängt davon ab, ob in anderen lexikalischen Umgebungen Verweise auf diese lexikalische Umgebung in Form von Links zu einer externen lexikalischen Umgebung vorhanden sind.
Detaillierte Analyse von Beispielen für die Arbeit mit Verschlüssen
Nachdem wir uns mit dem Wissen über den Ausführungskontext und die lexikalische Umgebung vertraut gemacht haben, werden wir zu Abschlüssen zurückkehren und dieselben Codefragmente, die wir bereits untersucht haben, genauer analysieren.
▍ Beispiel Nr. 1
Schauen Sie sich dieses Code-Snippet an:
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Wenn die
person()
-Funktion ausgeführt wird, erstellt die JS-Engine einen neuen Ausführungskontext und eine neue lexikalische Umgebung für diese Funktion. Nach Abschluss der Arbeit gibt die Funktion die Funktion
displayName()
. Ein Verweis auf diese Funktion wird in die Variable
peter
.
Ihre lexikalische Umgebung wird folgendermaßen aussehen:
personLexicalEnvironment = { environmentRecord: { name : 'Peter', displayName: < displayName function reference> } outer: <globalLexicalEnvironment> }
Wenn die Funktion
person()
wird, wird ihr Ausführungskontext vom Stapel entfernt. Die lexikalische Umgebung bleibt jedoch im Speicher, da in der lexikalischen Umgebung der internen Funktion
displayName()
eine Verknüpfung zu ihr besteht. Infolgedessen bleiben in dieser lexikalischen Umgebung deklarierte Variablen verfügbar.
Wenn die Funktion
peter()
aufgerufen wird (die entsprechende Variable speichert einen Verweis auf die Funktion
displayName()
), erstellt die JS-Engine einen neuen Ausführungskontext und eine neue lexikalische Umgebung für diese Funktion. Diese lexikalische Umgebung sieht folgendermaßen aus:
displayNameLexicalEnvironment = { environmentRecord: { } outer: <personLexicalEnvironment> }
Die Funktion
displayName()
keine Variablen, daher ist der Umgebungsdatensatz leer. Während der Ausführung dieser Funktion versucht die JS-Engine, die Namensvariable in der lexikalischen Umgebung der Funktion zu finden.
Da die Suche in der lexikalischen Umgebung der Funktion
displayName()
kann, wird die Suche in der externen lexikalischen Umgebung fortgesetzt,
displayName()
in der lexikalischen Umgebung der Funktion
person()
, die sich noch im Speicher befindet. Dort findet die Engine die gewünschte Variable und zeigt ihren Wert in der Konsole an.
▍ Beispiel Nr. 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
Die lexikalische Umgebung der Funktion
getCounter()
sieht folgendermaßen aus:
getCounterLexicalEnvironment = { environmentRecord: { counter: 0, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Diese Funktion gibt eine anonyme Funktion zurück, die der
count
zugewiesen ist.
Wenn die Funktion
count()
ausgeführt wird, sieht ihre lexikalische Umgebung folgendermaßen aus:
countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> }
Bei Ausführung dieser Funktion sucht das System in seiner lexikalischen Umgebung nach der Zählervariablen. Auch in diesem Fall ist der Funktionsumgebungsdatensatz leer, sodass die Suche nach der Variablen in der externen lexikalischen Umgebung der Funktion fortgesetzt wird.
Die Engine findet die Variable, zeigt sie in der Konsole an und erhöht die
getCounter()
, die in der lexikalischen Umgebung der Funktion
getCounter()
gespeichert ist.
Infolgedessen
getCounter()
die lexikalische Umgebung der Funktion
getCounter()
nach dem ersten Aufruf der Funktion
count()
aus:
getCounterLexicalEnvironment = { environmentRecord: { counter: 1, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Bei jedem Aufruf der Funktion
count()
erstellt die JavaScript-Engine eine neue lexikalische Umgebung für diese Funktion und erhöht die
getCounter()
, was zu Änderungen in der lexikalischen Umgebung der Funktion
getCounter()
.
Zusammenfassung
In diesem Artikel haben wir darüber gesprochen, was Verschlüsse sind, und die zugrunde liegenden JavaScript-Mechanismen aussortiert, die ihnen zugrunde liegen. Closures sind eines der wichtigsten grundlegenden JavaScript-Konzepte, und jeder JS-Entwickler sollte sie verstehen. Das Verständnis von Schließungen ist einer der Schritte zum Schreiben effektiver und qualitativ hochwertiger Anwendungen.
Liebe Leser! Wenn Sie Erfahrung in der JS-Entwicklung haben, teilen Sie bitte Anfängern praktische Beispiele für die Verwendung von Verschlüssen mit.
