JavaScript-Verknüpfungen für Anfänger

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(); // '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());  // 0 console.log(count());  // 1 console.log(count());  // 2 

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ührungskontext

Wenn 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 aufrufen

Wenn 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(); // '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());  // 0 console.log(count());  // 1 console.log(count());  // 2 

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.

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


All Articles