Verständnis der ECMAScript-Spezifikation, Teil 2
Lass uns unsere fantastischen Fähigkeiten im Lesen der Spezifikation weiter üben. Falls du dir die vorherige Folge noch nicht angeschaut hast, ist jetzt ein guter Zeitpunkt dafür!
Bereit für Teil 2?
Eine unterhaltsame Möglichkeit, die Spezifikation kennenzulernen, besteht darin, mit einer JavaScript-Funktion zu beginnen, von der wir wissen, dass sie existiert, und herauszufinden, wie sie spezifiziert ist.
Warnung! Diese Folge enthält kopierte Algorithmen aus der ECMAScript-Spezifikation von Februar 2020. Sie werden irgendwann veraltet sein.
Wir wissen, dass Eigenschaften in der Prototypenkette nachgeschlagen werden: Wenn ein Objekt die Eigenschaft, die wir lesen möchten, nicht hat, gehen wir die Prototypenkette hinauf, bis wir sie finden (oder ein Objekt finden, das keinen Prototyp mehr hat).
Zum Beispiel:
const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99
Wo ist der Prototypenlauf definiert?
Lass uns versuchen herauszufinden, wo dieses Verhalten definiert ist. Ein guter Ausgangspunkt ist eine Liste von Internen Methoden von Objekten.
Es gibt sowohl [[GetOwnProperty]]
als auch [[Get]]
— wir sind an der Version interessiert, die sich nicht nur auf eigene Eigenschaften beschränkt, also entscheiden wir uns für [[Get]]
.
Leider hat der Spezifikationstyp Property Descriptor ebenfalls ein Feld namens [[Get]]
, daher müssen wir beim Durchblättern der Spezifikation für [[Get]]
sorgfältig zwischen den beiden unabhängigen Verwendungen unterscheiden.
[[Get]]
ist eine wesentliche interne Methode. Normale Objekte implementieren das Standardverhalten für wesentliche interne Methoden. Exotische Objekte können ihre eigene interne Methode [[Get]]
definieren, die vom Standardverhalten abweicht. In diesem Beitrag konzentrieren wir uns auf normale Objekte.
Die Standardimplementierung von [[Get]]
delegiert an OrdinaryGet
:
Wenn die interne Methode
[[Get]]
des ObjektsO
mit dem EigenschaftsschlüsselP
und dem ECMAScript-WertReceiver
aufgerufen wird, werden die folgenden Schritte ausgeführt:
- Gib
? OrdinaryGet(O, P, Receiver)
zurück.
Wir werden gleich sehen, dass Receiver
der Wert ist, der als dieser Wert verwendet wird, wenn eine Getter-Funktion einer Accessor-Eigenschaft aufgerufen wird.
OrdinaryGet
ist folgendermaßen definiert:
OrdinaryGet ( O, P, Receiver )
Wenn die abstrakte Operation
OrdinaryGet
mit dem ObjektO
, dem EigenschaftsschlüsselP
und dem ECMAScript-WertReceiver
aufgerufen wird, werden die folgenden Schritte ausgeführt:
- Stelle sicher:
IsPropertyKey(P)
isttrue
.- Lass
desc
? O.[[GetOwnProperty]](P)
sein.- Wenn
desc
undefined
ist, dann
- Lass
parent
? O.[[GetPrototypeOf]]()
sein.- Wenn
parent
null
ist, gibundefined
zurück.- Gib
? parent.[[Get]](P, Receiver)
zurück.- Wenn
IsDataDescriptor(desc)
true
ist, gibdesc.[[Value]]
zurück.- Stelle sicher:
IsAccessorDescriptor(desc)
isttrue
.- Lass
getter
desc.[[Get]]
sein.- Wenn
getter
undefined
ist, gibundefined
zurück.- Gib
? Call(getter, Receiver)
zurück.
Der Prototypenkettenlauf ist in Schritt 3 enthalten: Wenn wir die Eigenschaft nicht als eigene Eigenschaft finden, rufen wir die [[Get]]
-Methode des Prototyps auf, die erneut an OrdinaryGet
delegiert. Wenn wir die Eigenschaft immer noch nicht finden, rufen wir die [[Get]]
-Methode ihres Prototyps auf, die erneut an OrdinaryGet
delegiert, und so weiter, bis wir entweder die Eigenschaft finden oder ein Objekt ohne Prototyp erreichen.
Lass uns ansehen, wie dieser Algorithmus funktioniert, wenn wir o2.foo
aufrufen. Zuerst rufen wir OrdinaryGet
mit O
als o2
und P
als "foo"
auf. O.[[GetOwnProperty]]("foo")
gibt undefined
zurück, da o2
keine eigene Eigenschaft namens "foo"
hat, also nehmen wir den If-Zweig in Schritt 3. In Schritt 3.a setzen wir parent
auf den Prototypen von o2
, der o1
ist. parent
ist nicht null
, also kehren wir in Schritt 3.b nicht zurück. In Schritt 3.c rufen wir die [[Get]]
-Methode des Elternteils mit dem Eigenschaftsschlüssel "foo"
auf und geben zurück, was sie zurückgibt.
Das Elternobjekt (o1
) ist ein normales Objekt, daher ruft seine [[Get]]
-Methode erneut OrdinaryGet
auf, diesmal mit O
als o1
und P
als "foo"
. o1
hat eine eigene Eigenschaft namens "foo"
, also gibt in Schritt 2 O.[[GetOwnProperty]]("foo")
den zugehörigen Eigenschaftsdeskriptor zurück, und wir speichern ihn in desc
.
Eigenschaftsbeschreiber ist ein Spezifikationstyp. Daten-Eigenschaftsbeschreiber speichern den Wert der Eigenschaft direkt im [[Value]]
-Feld. Zugriff-Eigenschaftsbeschreiber speichern die Zugriffsfunktionsfelder in [[Get]]
und/oder [[Set]]
. In diesem Fall ist der Eigenschaftsbeschreiber, der "foo"
zugeordnet ist, ein Daten-Eigenschaftsbeschreiber.
Der Daten-Eigenschaftsbeschreiber, den wir in Schritt 2 in desc
gespeichert haben, ist nicht undefined
, daher gehen wir in Schritt 3 nicht in die if
-Verzweigung. Als Nächstes führen wir Schritt 4 aus. Der Eigenschaftsbeschreiber ist ein Daten-Eigenschaftsbeschreiber, daher geben wir in Schritt 4 sein [[Value]]
-Feld, 99
, zurück, und das war's.
Was ist Receiver
und woher stammt es?
Der Receiver
-Parameter wird nur im Fall von Zugriffseigenschaften in Schritt 8 verwendet. Es wird als this-Wert übergeben, wenn die Getter-Funktion einer Zugriffseigenschaft aufgerufen wird.
OrdinaryGet
übergibt den ursprünglichen Receiver
während der Rekursion unverändert (Schritt 3.c). Schauen wir uns an, woher der ursprüngliche Receiver
stammt!
Wenn wir nach Stellen suchen, an denen [[Get]]
aufgerufen wird, finden wir eine abstrakte Operation GetValue
, die auf Referenzen arbeitet. Eine Referenz ist ein Spezifikationstyp, der aus einem Basiswert, dem referenzierten Namen und einer strikten Referenzmarkierung besteht. Im Fall von o2.foo
ist der Basiswert das Objekt o2
, der referenzierte Name ist der String "foo"
, und die strikte Referenzmarkierung ist false
, da der Beispielcode nachlässig ist.
Abstecher: Warum ist Referenz kein Record?
Abstecher: Eine Referenz ist kein Record, auch wenn es den Anschein erweckt. Sie besteht aus drei Komponenten, die genauso gut als drei benannte Felder ausgedrückt werden könnten. Eine Referenz ist nur aus historischen Gründen kein Record.
Zurück zu GetValue
Schauen wir uns an, wie GetValue
definiert ist:
ReturnIfAbrupt(V)
.- Wenn
Type(V)
nichtReference
ist, gibV
zurück.- Lass
base
gleichGetBase(V)
sein.- Wenn
IsUnresolvableReference(V)
gleichtrue
ist, löse eineReferenceError
-Ausnahme aus.- Wenn
IsPropertyReference(V)
gleichtrue
ist, dann
- Wenn
HasPrimitiveBase(V)
gleichtrue
ist, dann
- Stelle sicher: In diesem Fall wird
base
niemalsundefined
odernull
sein.- Setze
base
auf! ToObject(base)
.- Gib
? base.[[Get]](GetReferencedName(V), GetThisValue(V))
zurück.- Andernfalls
- Stelle sicher:
base
ist eine Environment Record.- Gib
? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))
zurück.
Die Referenz in unserem Beispiel ist o2.foo
, was eine Eigenschaftsreferenz ist. Daher nehmen wir den Zweig 5. Wir folgen nicht dem Zweig 5.a, da die Basis (o2
) kein primitiver Wert ist (eine Zahl, ein String, ein Symbol, ein BigInt, ein Boolean, undefined
oder null
).
Dann rufen wir [[Get]]
in Schritt 5.b auf. Der Receiver
, den wir übergeben, ist GetThisValue(V)
. In diesem Fall ist das einfach der Basiswert der Referenz:
- Stelle sicher:
IsPropertyReference(V)
isttrue
.- Wenn
IsSuperReference(V)
gleichtrue
ist, dann
- Gib den Wert der
thisValue
-Komponente der ReferenzV
zurück.- Gib
GetBase(V)
zurück.
Für o2.foo
folgen wir nicht dem Zweig in Schritt 2, da es sich nicht um eine Super-Referenz handelt (zum Beispiel super.foo
), sondern wir nehmen Schritt 3 und geben den Basiswert der Referenz zurück, der o2
ist.
Alles zusammen genommen stellen wir fest, dass wir den Receiver
auf die Basis der ursprünglichen Referenz setzen und ihn dann unverändert während des Durchlaufens der Prototypen-Kette halten. Wenn die gefundene Eigenschaft schließlich eine Zugriffseigenschaft ist, verwenden wir den Receiver
als this-Wert, wenn wir ihn aufrufen.
Insbesondere bezieht sich der this-Wert in einem Getter auf das ursprüngliche Objekt, von dem wir versucht haben, die Eigenschaft abzurufen, nicht auf das Objekt, in dem wir die Eigenschaft während des Durchlaufens der Prototypen-Kette gefunden haben.
Probieren wir es aus!
const o1 = { x: 10, get foo() { return this.x; } };
const o2 = { x: 50 };
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 50
In diesem Beispiel haben wir eine Zugriffseigenschaft namens foo
und definieren einen Getter dafür. Der Getter gibt this.x
zurück.
Dann greifen wir auf o2.foo
zu - was gibt der Getter zurück?
Wir haben herausgefunden, dass beim Aufrufen des Getters der this-Wert das Objekt ist, von dem wir ursprünglich versucht haben, die Eigenschaft abzurufen, nicht das Objekt, in dem die Eigenschaft gefunden wurde. In diesem Fall ist der this-Wert o2
, nicht o1
. Wir können dies überprüfen, indem wir sicherstellen, ob der Getter o2.x
oder o1.x
zurückgibt. Tatsächlich gibt er o2.x
zurück.
Es funktioniert! Wir waren in der Lage, das Verhalten dieses Codeausschnitts basierend auf dem, was wir in der Spezifikation gelesen haben, vorherzusehen.
Zugriff auf Eigenschaften — warum ruft es [[Get]]
auf?
Wo sagt die Spezifikation, dass die interne Objektmethode [[Get]]
aufgerufen wird, wenn auf eine Eigenschaft wie o2.foo
zugegriffen wird? Das muss doch irgendwo definiert sein. Vertrauen Sie nicht einfach meinem Wort!
Wir haben herausgefunden, dass die interne Objektmethode [[Get]]
von der abstrakten Operation GetValue
aufgerufen wird, die auf Referenzen arbeitet. Aber von wo aus wird GetValue
aufgerufen?
Laufzeitsemantik für MemberExpression
Die grammatikalischen Regeln der Spezifikation definieren die Syntax der Sprache. Laufzeitsemantik definieren, was die syntaktischen Konstrukte „bedeuten“ (wie sie zur Laufzeit ausgewertet werden).
Wenn Sie mit kontextfreien Grammatiken nicht vertraut sind, sollten Sie jetzt einen Blick darauf werfen!
Wir werden uns die grammatikalischen Regeln in einer späteren Episode genauer ansehen. Halten wir es jetzt erst einmal einfach! Insbesondere können wir in dieser Episode die Indizes (Yield
, Await
usw.) in den Produktionen ignorieren.
Die folgenden Produktionen beschreiben, wie ein MemberExpression
aussieht:
MemberExpression :
PrimaryExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
MemberExpression TemplateLiteral
SuperProperty
MetaProperty
new MemberExpression Arguments
Hier haben wir 7 Produktionen für MemberExpression
. Eine MemberExpression
kann einfach eine PrimaryExpression
sein. Alternativ kann eine MemberExpression
aus einer anderen MemberExpression
und einer Expression
zusammengesetzt werden: MemberExpression [ Expression ]
, z.B. o2['foo']
. Oder sie kann MemberExpression . IdentifierName
sein, z.B. o2.foo
– dies ist die Produktion, die für unser Beispiel relevant ist.
Die Laufzeitsemantik für die Produktion MemberExpression : MemberExpression . IdentifierName
definiert die Schritte, die bei der Auswertung durchzuführen sind:
Laufzeitsemantik: Evaluation für
MemberExpression : MemberExpression . IdentifierName
- Lasse
baseReference
das Ergebnis der Auswertung vonMemberExpression
sein.- Lasse
baseValue
? GetValue(baseReference)
sein.- Wenn der von dieser
MemberExpression
abgedeckte Code strikter Modus-Code ist, setzestrict
auftrue
; andernfalls setzestrict
auffalse
.- Gib
? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict)
zurück.
Der Algorithmus delegiert an die abstrakte Operation EvaluatePropertyAccessWithIdentifierKey
, also müssen wir diese ebenfalls lesen:
EvaluatePropertyAccessWithIdentifierKey( baseValue, identifierName, strict )
Die abstrakte Operation
EvaluatePropertyAccessWithIdentifierKey
nimmt als Argumente einen WertbaseValue
, einen Parse-KnotenidentifierName
und ein boolesches Argumentstrict
. Sie führt die folgenden Schritte aus:
- Behauptung:
identifierName
ist einIdentifierName
.- Lasse
bv
? RequireObjectCoercible(baseValue)
sein.- Lasse
propertyNameString
denStringValue
vonidentifierName
sein.- Gib einen Wert vom Typ Reference zurück, dessen Basiskomponente
bv
, dessen referenzierter NamepropertyNameString
und dessen Strikt-Referenz-Flagstrict
ist.
Das bedeutet: EvaluatePropertyAccessWithIdentifierKey
erstellt eine Reference, die den bereitgestellten baseValue
als Basis, den Zeichenkettenwert von identifierName
als Eigenschaftsnamen und strict
als strikten Modus-Flag verwendet.
Letztendlich wird diese Reference an GetValue
übergeben. Dies wird an mehreren Stellen in der Spezifikation definiert, abhängig davon, wie die Reference verwendet wird.
MemberExpression
als Parameter
In unserem Beispiel verwenden wir den Eigenschaftszugriff als Parameter:
console.log(o2.foo);
In diesem Fall ist das Verhalten in der Laufzeitsemantik der ArgumentList
-Produktion definiert, die GetValue
für das Argument aufruft:
Laufzeitsemantik:
ArgumentListEvaluation
ArgumentList : AssignmentExpression
- Lasse
ref
das Ergebnis der Auswertung vonAssignmentExpression
sein.- Lasse
arg
? GetValue(ref)
sein.- Gib eine Liste zurück, deren einziges Element
arg
ist.
o2.foo
sieht nicht wie eine AssignmentExpression
aus, aber es ist eine, also ist diese Produktion anwendbar. Um herauszufinden, warum, können Sie sich diesen zusätzlichen Inhalt ansehen, aber es ist an dieser Stelle nicht unbedingt erforderlich.
Die AssignmentExpression
in Schritt 1 ist o2.foo
. ref
, das Ergebnis der Auswertung von o2.foo
, ist die oben erwähnte Reference. In Schritt 2 rufen wir GetValue
darauf auf. Somit wissen wir, dass die interne Objekt-Methode [[Get]]
aufgerufen wird und der Prototype-Chain-Walk durchgeführt wird.
Zusammenfassung
In dieser Episode haben wir untersucht, wie die Spezifikation ein Sprachmerkmal definiert, in diesem Fall die Prototype-Suche, über alle verschiedenen Ebenen hinweg: die syntaktischen Konstrukte, die das Merkmal auslösen, und die es definierenden Algorithmen.