In diesem Kapitel untersuchen wir genauer, wie die ECMAScript-Sprachspezifikation mit Variablen umgeht.
Eine Umgebung ist die Datenstruktur, die die ECMAScript-Spezifikation zur Verwaltung von Variablen verwendet. Es handelt sich um ein Wörterbuch, dessen Schlüssel Variablennamen und dessen Werte die Werte dieser Variablen sind. Jeder Scope hat seine zugehörige Umgebung. Umgebungen müssen die folgenden Phänomene im Zusammenhang mit Variablen unterstützen können:
Wir werden anhand von Beispielen veranschaulichen, wie dies für jedes Phänomen geschieht.
Wir beginnen mit der Rekursion. Betrachten Sie den folgenden Code:
function f(x) {
return x * 2;
}
function g(y) {
const tmp = y + 1;
return f(tmp);
}
assert.equal(g(3), 8);Für jeden Funktionsaufruf benötigen Sie frischen Speicherplatz für die Variablen (Parameter und lokale Variablen) der aufgerufenen Funktion. Dies wird über einen Stack von sogenannten Ausführungskontexten verwaltet, die (für den Zweck dieses Kapitels) Verweise auf Umgebungen sind. Die Umgebungen selbst werden auf dem Heap gespeichert. Das ist notwendig, weil sie gelegentlich länger leben, nachdem die Ausführung ihre Scopes verlassen hat (das sehen wir bei der Untersuchung von Closures). Daher können sie selbst nicht über einen Stack verwaltet werden.
Bei der Ausführung des Codes machen wir folgende Pausen:
function f(x) {
// Pause 3
return x * 2;
}
function g(y) {
const tmp = y + 1;
// Pause 2
return f(tmp);
}
// Pause 1
assert.equal(g(3), 8);Dies ist, was passiert
Pause 1 – vor dem Aufruf von g() (Abb. 1).
Pause 2 – während der Ausführung von g() (Abb. 2).
Pause 3 – während der Ausführung von f() (Abb. 3).
Verbleibende Schritte: Jedes Mal, wenn ein return auftritt, wird ein Ausführungskontext vom Stack entfernt.
g(): Der Ausführungskontext-Stack hat einen Eintrag, der auf die Top-Level-Umgebung verweist. In dieser Umgebung gibt es zwei Einträge; einen für f() und einen für g().g(): Die Spitze des Ausführungskontext-Stacks verweist auf die für g() erstellte Umgebung. Diese Umgebung enthält Einträge für das Argument y und für die lokale Variable tmp.f(): Der oberste Ausführungskontext verweist nun auf die Umgebung für f().Wir verwenden den folgenden Code, um zu untersuchen, wie verschachtelte Scopes über Umgebungen implementiert werden.
function f(x) {
function square() {
const result = x * x;
return result;
}
return square();
}
assert.equal(f(6), 36);Hier haben wir drei verschachtelte Scopes: den Top-Level-Scope, den Scope von f() und den Scope von square(). Beobachtungen:
Daher verweist die Umgebung jedes Scopes über ein Feld namens outer auf die Umgebung des umgebenden Scopes. Wenn wir den Wert einer Variablen nachschlagen, suchen wir zuerst nach ihrem Namen in der aktuellen Umgebung, dann in der äußeren Umgebung, dann in der äußeren Umgebung ihrer äußeren Umgebung usw. Die gesamte Kette von äußeren Umgebungen enthält alle Variablen, auf die aktuell zugegriffen werden kann (abzüglich überschatteter Variablen).
Wenn Sie einen Funktionsaufruf tätigen, erstellen Sie eine neue Umgebung. Die äußere Umgebung dieser Umgebung ist die Umgebung, in der die Funktion erstellt wurde. Um bei der Einrichtung des Feldes outer von über Funktionsaufrufe erstellten Umgebungen zu helfen, hat jede Funktion eine interne Eigenschaft namens [[Scope]], die auf ihre "Geburtsumgebung" verweist.
Dies sind die Pausen, die wir bei der Ausführung des Codes machen.
function f(x) {
function square() {
const result = x * x;
// Pause 3
return result;
}
// Pause 2
return square();
}
// Pause 1
assert.equal(f(6), 36);Dies ist, was passiert
f() (Abb. 4).f() (Abb. 5).square() (Abb. 6).return-Anweisungen Ausführungseinträge vom Stack.f(): Die Top-Level-Umgebung hat einen einzelnen Eintrag für f(). Die Geburtsumgebung von f() ist die Top-Level-Umgebung. Daher verweist f's [[Scope]] darauf.f(): Es gibt jetzt eine Umgebung für den Funktionsaufruf f(6). Die äußere Umgebung dieser Umgebung ist die Geburtsumgebung von f() (die Top-Level-Umgebung an Index 0). Wir sehen, dass das Feld outer auf den Wert von f's [[Scope]] gesetzt wurde. Darüber hinaus ist das [[Scope]] der neuen Funktion square() die gerade erstellte Umgebung.square(): Das vorherige Muster wurde wiederholt: das outer der neuesten Umgebung wurde über das [[Scope]] der gerade aufgerufenen Funktion eingerichtet. Die Kette von Scopes, die über outer erstellt wurden, enthält alle Variablen, die gerade aktiv sind. Wir können zum Beispiel auf result, square und f zugreifen, wenn wir möchten. Umgebungen spiegeln zwei Aspekte von Variablen wider. Erstens spiegelt die Kette von äußeren Umgebungen die verschachtelten statischen Scopes wider. Zweitens spiegelt der Stack von Ausführungskontexten wider, welche Funktionsaufrufe dynamisch getätigt wurden.Um zu sehen, wie Umgebungen zur Implementierung von Closures verwendet werden, verwenden wir das folgende Beispiel:
Was passiert hier? add() ist eine Funktion, die eine Funktion zurückgibt. Wenn wir den verschachtelten Funktionsaufruf add(3)(1) in Zeile B machen, ist der erste Parameter für add(), der zweite Parameter für die von ihr zurückgegebene Funktion. Das funktioniert, weil die in Zeile A erstellte Funktion die Verbindung zu ihrem Geburts-Scope nicht verliert, wenn sie diesen Scope verlässt. Die zugehörige Umgebung wird durch diese Verbindung am Leben gehalten und die Funktion hat weiterhin Zugriff auf die Variable x in dieser Umgebung (x ist innerhalb der Funktion frei).
Diese verschachtelte Art, add() aufzurufen, hat einen Vorteil: Wenn Sie nur den ersten Funktionsaufruf tätigen, erhalten Sie eine Version von add(), bei der der Parameter x bereits ausgefüllt ist.
Die Umwandlung einer Funktion mit zwei Parametern in zwei verschachtelte Funktionen mit jeweils einem Parameter wird als Currying bezeichnet. add() ist eine gecurriede Funktion.
Nur einige der Parameter einer Funktion auszufüllen, wird als partielle Anwendung bezeichnet (die Funktion wurde noch nicht vollständig angewendet). Die Methode .bind() von Funktionen führt eine partielle Anwendung durch. Im vorherigen Beispiel sehen wir, dass die partielle Anwendung einfach ist, wenn eine Funktion gecurried ist.
Während wir den folgenden Code ausführen, machen wir drei Pausen:
function add(x) {
return (y) => {
// Pause 3: plus2(5)
return x + y;
}; // Pause 1: add(2)
}
const plus2 = add(2);
// Pause 2
assert.equal(plus2(5), 7);Dies ist, was passiert
add(2) (Abb. 7).add(2) (Abb. 8).plus2(5) (Abb. 9).add(2): Wir sehen, dass die von add() zurückgegebene Funktion bereits existiert (siehe unten rechts) und dass sie über ihre interne Eigenschaft [[Scope]] auf ihre Geburtsumgebung verweist. Beachten Sie, dass sich plus2 noch in seiner temporären toten Zone befindet und uninitialisiert ist.add(2): plus2 verweist nun auf die von add(2) zurückgegebene Funktion. Diese Funktion hält ihre Geburtsumgebung (die Umgebung von add(2)) über ihr [[Scope]] am Leben.plus2(5): Das [[Scope]] von plus2 wird verwendet, um das outer der neuen Umgebung einzurichten. So erhält die aktuelle Funktion Zugriff auf x.