Hallo Habr!
In einem
früheren Artikel haben wir die allgemeine Theorie von OOP in Bezug auf EcmaScript und den populären Irrtum von unerfahrenen Entwicklern hinsichtlich der Unterschiede zwischen OOP in JS und klassischen Sprachen untersucht.
Heute werden wir über zwei andere gleich wichtige EcmaScript-Konzepte sprechen, nämlich die Beziehung der Entität zum Ausführungskontext (
dies ist diese Verbindung) und die Beziehung der Entität zum generierenden Kontext (
ScopeChain ).
Also fangen wir an!
das
Bei Interviews als Antwort auf die Frage: "Erzählen Sie uns mehr darüber." Anfängerentwickler geben in der Regel sehr vage Antworten: "
Dies ist das Objekt" vor dem Punkt ", mit dem die Methode aufgerufen wurde", "
Dies ist der Kontext, in dem die Funktion aufgerufen wurde" usw. ...
Tatsächlich ist die Situation mit diesem Konzept, das für EcmaScript von zentraler Bedeutung ist, etwas komplizierter. Lassen Sie es uns in der richtigen Reihenfolge herausfinden.
Angenommen, wir haben ein JavaScript-Programm, dessen Variablen global deklariert sind. globale Funktionen; lokale Funktionen (innerhalb anderer Funktionen deklariert), von Funktionen zurückgegebene Funktionen.
const a = 10; const b = 20; const x = { a: 15, b: 25, } function foo(){ return this.a + this.b; } function bar () { const a = 30; return a + b; } function fooBaz(){ function test () { return this.a + this.b; } return test(); } function fooBar() { const a = 40; const b = 50; return function () { return a + b; } } fooBar()();
Bei der Übertragung der Steuerung auf den ausführbaren Code wird ein Eintrag in den Ausführungskontext vorgenommen. Ausführbarer Code - Dies ist jeder Code, den wir zu einem bestimmten Zeitpunkt ausführen. Es kann sich um einen globalen Code oder einen Code einer beliebigen Funktion handeln.
Der Ausführungskontext ist eine Abstraktion, die Code typisiert und begrenzt. Unter dem Gesichtspunkt dieser Abstraktion ist der Code in globalen (alle verbundenen Skripte, Inline-Skripte) und Funktionscode (der Code verschachtelter Funktionen gehört nicht zum Kontext übergeordneter Funktionen) unterteilt.
Es gibt einen dritten Typ - EvalCode. In diesem Artikel vernachlässigen wir es.
Logischerweise ist die Menge der Ausführungskontexte ein
Stapel , der nach dem Prinzip des Last-in-First-out (lifo) arbeitet. Das Ende des Stapels ist immer der globale Kontext, und das Ende ist die aktuelle ausführbare Datei. Bei jedem Aufruf einer Funktion wird ein Eintrag in ihren Kontext vorgenommen. Wenn eine Funktion abgeschlossen ist, endet ihr Kontext. Verbrauchte Kontexte werden nacheinander und in umgekehrter Reihenfolge vom Stapel entfernt.
Schauen Sie sich den obigen Code an. Wir haben einen Aufruf der Funktion
fooBar im globalen Code. In der Funktion
fooBar geben wir
eine anonyme Funktion zurück , die wir sofort aufrufen. Die folgenden Änderungen treten beim Stapel auf: Ein
globaler Kontext wird in ihn
eingefügt - wenn
fooBar aufgerufen wird
, wird sein Kontext in den Stapel aufgenommen -
fooBar wird beendet, gibt
eine anonyme Funktion zurück und wird aus dem Stapel entfernt - eine
anonyme Funktion wird aufgerufen, ihr Kontext wird in den Stapel aufgenommen - eine
anonyme Funktion erfüllt, gibt einen Wert zurück und sein Kontext wird aus dem Stapel gelöscht - am Ende des Skripts wird der
globale Kontext aus dem Stapel gelöscht.
Der Ausführungskontext kann bedingt als Objekt dargestellt werden. Eine der Eigenschaften dieses Objekts ist die Lexical Environment (LO).
Die lexikalische Umgebung enthält:
- alle Kontextvariablendeklarationen
- alle Funktionsdeklarationen
- alle formalen Parameter der Funktion (wenn wir über den Kontext von Funktionen sprechen)
Bei der Eingabe des Ausführungskontexts scannt der Interpreter den Kontext. Alle Variablendeklarationen und Funktionsdeklarationen stehen am Anfang des Kontexts. Variablen werden gleich undefiniert erstellt und Funktionen sind vollständig einsatzbereit.
Dies ist auch eine Eigenschaft des Ausführungskontexts, aber nicht des Kontexts selbst, wie einige unerfahrene Interviewer antworten!
Dies wird beim Eingeben des Kontexts definiert und bleibt bis zum Ende der Lebensdauer des Kontexts unverändert (bis der Kontext vom Stapel entfernt wird).
Im globalen Ausführungskontext wird
dies durch den
strengen Modus bestimmt : Wenn der strenge Modus deaktiviert ist, enthält dieser ein globales Objekt (im Browser wird es auf die oberste Ebene im Fensterobjekt übertragen), wobei 'use strict' undefiniert ist.
dies im Kontext von Funktionen - die Frage ist viel interessanter!
Diese Funktion wird vom Aufrufer festgelegt und hängt von der Syntax des Aufrufs ab. Wie wir wissen, gibt es beispielsweise Methoden, mit denen dies beim
Aufruf starr festgelegt werden kann (
Aufruf ,
Anwenden ), und eine Methode, mit der Sie einen Wrapper mit "Fixed This" (
Binden ) erstellen können. In diesen Situationen geben wir dies ausdrücklich an und es kann kein Zweifel an seiner Definition bestehen.
Bei einem normalen Funktionsaufruf ist die Situation viel komplizierter!
Einer der in EcmaScript integrierten Typen,
ReferenceType , hilft uns zu verstehen, wie dies in Funktionen angebracht ist. Dies ist einer der internen Typen, die auf Implementierungsebene verfügbar sind. Logischerweise handelt es sich um ein Objekt mit zwei Eigenschaften
base (eine Referenz auf ein bestimmtes Basisobjekt, für das ein ReferenceType zurückgegeben wird),
propertyName (eine Zeichenfolgendarstellung des Bezeichners des Objekts, für das ein ReferenceType zurückgegeben wird).
ReferenceType wird für alle Variablendeklarationen, Funktionsdeklarationen und Eigenschaftsreferenzen zurückgegeben (dies ist der Fall, der uns unter dem Gesichtspunkt des Verständnisses interessiert).
Die Regel, um
dies für Funktionen zu definieren, die auf die übliche Weise aufgerufen werden:
Befindet sich der ReferenceType links von den Aktivierungsklammern der Funktion, wird die Basis dieses ReferenceType in this
Funktion eingefügt. Befindet sich ein anderer Typ links von den Klammern, this
es sich entweder um ein globales Objekt oder um ein undefined
Objekt (tatsächlich null
, aber da null aus Sicht von Ecmascript keinen bestimmten Wert hat, wird es in ein globales Objekt umgewandelt, auf das möglicherweise verwiesen wird entspricht undefined
je nach striktem Modus).Schauen wir uns ein Beispiel an:
const x = 0; const obj = { x: 10, foo: function() { return this.x; } } obj.foo();
Ich denke, die Definitionsmethode ist klar dargestellt. Betrachten Sie nun einige weniger offensichtliche Fälle.
Funktionsausdrücke
Kehren wir für eine Sekunde zu unserem Referenztyp zurück. Dieser Typ verfügt über eine integrierte
GetValue- Methode, die den wahren Typ des über den ReferenceType empfangenen Objekts zurückgibt. In der Ausdruckszone wird GetValue immer ausgelöst.
Ein Beispiel:
(function (){ return this;
Dies liegt daran, dass GetValue immer in der Ausdruckszone ausgelöst wird. GetValue gibt einen Funktionstyp zurück und links von den Aktivierungsklammern befindet sich kein ReferenceType. Erinnern Sie sich an unsere Regel, um
dies zu bestimmen:
Wenn sich ein anderer Typ links von den Klammern befindet, wird ein globales Objekt in this
oder undefined
(tatsächlich null
, aber da null aus Sicht von Ecmascript keinen bestimmten Wert hat, wird es in ein globales Objekt konvertiert , dessen Verknüpfung je nach striktem Modus gleich undefiniert sein kann) .
Ausdruckszonen sind: Zuweisung (=), Operatoren || oder andere logische Operatoren, ternärer Operator, Array-Initialisierer, durch Kommas getrennte Liste.
const x = 0; const obj = { x: 10, foo: function() { return this.x; } } obj.foo();
Identische Situation in benannten funktionalen Ausdrücken. Auch bei einem rekursiven Aufruf dieses globalen Objekts oder
undefined
Diese verschachtelten Funktionen werden im übergeordneten Element aufgerufen
Auch eine wichtige Situation!
const x = 0; function foo() { function bar(){ return this.x; } return bar(); } const obj = {x:10}; obj.test = foo; obj.test();
Dies liegt daran, dass der Aufruf von
bar()
dem Aufruf von
LE_foo.bar
und das Objekt der lexikalischen Umgebung dies nicht definiert.
Konstruktorfunktionen
Wie ich oben geschrieben habe:
Diese Funktion wird vom Aufrufer festgelegt und hängt von der Syntax des Aufrufs ab.
Wir rufen Konstruktorfunktionen mit dem neuen Schlüsselwort auf. Die Besonderheit dieser Methode zur Funktionsaktivierung besteht darin, dass die interne Funktionsmethode
[[Konstrukt]] aufgerufen wird, die bestimmte Operationen ausführt (der Mechanismus zum Erstellen von Entitäten durch Designer wird im zweiten oder dritten Artikel über OOP erläutert!) Und die interne
[[Aufruf]] -Methode aufruft, die niedergelegt wird in
dieser erstellten Instanz der Konstruktorfunktion.
Scope Chain
Die Scope-Kette ist auch eine Eigenschaft des Ausführungskontexts wie dieser. Es ist eine Liste von Objekten der lexikalischen Umgebungen des aktuellen Kontexts und aller generierenden Kontexte. In dieser Kette erfolgt die Suche nach Variablen beim Auflösen von Bezeichnernamen.
Hinweis: Dadurch wird eine Funktion einem Ausführungskontext und ScopeChain einem untergeordneten Kontext zugeordnet.
Die Spezifikation besagt, dass ScopeChain ein Array ist:
SC = [LO, LO1, LO2,..., LOglobal];
In einigen Implementierungen, wie z. B. JS, wird die Bereichskette jedoch über
verknüpfte Listen implementiert.
Um ScopeChain besser zu verstehen, werden wir den Lebenszyklus von Funktionen diskutieren. Es ist in die Erstellungsphase und die Ausführungsphase unterteilt.
Wenn eine Funktion erstellt wird, wird ihr die interne Eigenschaft
[[SCOPE]] zugewiesen.
In
SCOPE wird eine hierarchische Kette von Objekten aus lexikalischen Umgebungen mit höheren (generierenden) Kontexten aufgezeichnet. Diese Eigenschaft bleibt unverändert, bis die Funktion vom Garbage Collector zerstört wird.
Beachten Sie!
[[SCOPE]] ist im Gegensatz zu ScopeChain eine Eigenschaft der Funktion selbst, nicht ihres Kontexts.
Wenn eine Funktion aufgerufen wird, wird ihr Ausführungskontext initialisiert und gefüllt. Der Kontext ist mit ScopeChain = LO (der Funktion selbst) + [[SCOPE]] (hierarchische Kette von LO, die Kontexte beeinflusst) verbunden.
Auflösung von Bezeichnernamen - sequentielle Abfrage von
LO- Objekten in der
ScopeChain- Kette
von links nach rechts. Die Ausgabe ist ein ReferenceType, dessen Basiseigenschaft auf das LO-Objekt verweist, in dem der Bezeichner gefunden wurde, und PropertyName ist eine Zeichenfolgendarstellung des Bezeichnernamens.
So ist der Verschluss unter der Haube angeordnet! Ein Abschluss ist im Wesentlichen das Ergebnis einer Suche in ScopeChain nach allen Variablen, deren Bezeichner in der Funktion vorhanden sind.
const x = 10; function foo () { return x; } (function (){ const x = 20; foo();
Das folgende Beispiel zeigt den Lebenszyklus
[[SCOPE]] .
function foo () { const x = 10; const y = 20; return function () { return [x,y]; } } const x = 30; const bar = foo();
Eine wichtige Ausnahme ist
die Konstruktorfunktion . Bei dieser Art von Funktion zeigt [[SCOPE]] immer auf ein globales Objekt.
Vergessen Sie auch nicht, dass die Suche auch im Prototyp durchgeführt wird, wenn eines der Glieder in der ScopeChain-Kette einen Prototyp hat.
Fazit
Wir werden die Schlüsselideen theoretisch darlegen:
- Dies ist die Beziehung der Entität zum Ausführungskontext
- ScopeChain ist die Beziehung einer Entität zu allen Spawning-Kontexten
- this und ScopeChain sind Ausführungskontexteigenschaften
- Diese Funktion wird vom Anrufer festgelegt und hängt von der Syntax des Anrufs ab
- ScopeChain ist die lexikalische Umgebung des aktuellen Kontexts + [[Scope]]
- [[Scope]] - Dies ist eine Eigenschaft der Funktion selbst und enthält eine hierarchische Kette von lexikalischen Umgebungen zum Generieren von Kontexten
Hoffe der Artikel war hilfreich. Bis zu zukünftigen Artikeln, Freunde!