Verstehen der ECMAScript-Spezifikation, Teil 1
In diesem Artikel nehmen wir eine einfache Funktion in der Spezifikation und versuchen, die Notation zu verstehen. Los geht's!
Vorwort
Auch wenn Sie JavaScript kennen, kann das Lesen der Sprachspezifikation, der ECMAScript-Sprachspezifikation, kurz ECMAScript-Spezifikation, ziemlich einschüchternd sein. Zumindest so habe ich mich gefühlt, als ich sie das erste Mal gelesen habe.
Beginnen wir mit einem konkreten Beispiel und gehen durch die Spezifikation, um es zu verstehen. Der folgende Code zeigt die Verwendung von Object.prototype.hasOwnProperty
:
const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false
Im Beispiel hat o
keine Eigenschaft namens hasOwnProperty
, also gehen wir die Prototypkette entlang und suchen danach. Wir finden sie im Prototyp von o
, der Object.prototype
ist.
Um zu beschreiben, wie Object.prototype.hasOwnProperty
funktioniert, verwendet die Spezifikation Pseudo-Code-ähnliche Beschreibungen:
Object.prototype.hasOwnProperty(V)
Wenn die Methode
hasOwnProperty
mit dem ArgumentV
aufgerufen wird, werden die folgenden Schritte ausgeführt:
- Setze
P
auf? ToPropertyKey(V)
.- Setze
O
auf? ToObject(this value)
.- Gib
? HasOwnProperty(O, P)
zurück.
…und…
Die abstrakte Operation
HasOwnProperty
dient dazu zu bestimmen, ob ein Objekt eine eigene Eigenschaft mit dem angegebenen Eigenschaftsschlüssel hat. Ein Boolescher Wert wird zurückgegeben. Die Operation wird mit den ArgumentenO
undP
aufgerufen, wobeiO
das Objekt undP
der Eigenschaftsschlüssel ist. Diese abstrakte Operation führt die folgenden Schritte aus:
- Behauptung:
Type(O)
istObject
.- Behauptung:
IsPropertyKey(P)
isttrue
.- Setze
desc
auf? O.[[GetOwnProperty]](P)
.- Wenn
desc
undefined
ist, gibfalse
zurück.- Gib
true
zurück.
Aber was ist eine „abstrakte Operation“? Was sind die Dinge in [[ ]]
? Warum steht ein ?
vor einer Funktion? Was bedeuten die Behauptungen?
Finden wir es heraus!
Sprachtypen und Spezifikationstypen
Fangen wir mit etwas Vertrautem an. Die Spezifikation verwendet Werte wie undefined
, true
und false
, die wir bereits aus JavaScript kennen. Sie sind alle Sprachwerte, Werte von Sprachtypen, die in der Spezifikation ebenfalls definiert sind.
Die Spezifikation verwendet Sprachwerte auch intern, zum Beispiel könnte ein interner Datentyp ein Feld enthalten, dessen mögliche Werte true
und false
sind. Im Gegensatz dazu verwenden JavaScript-Engines normalerweise keine Sprachwerte intern. Wenn die JavaScript-Engine beispielsweise in C++ geschrieben ist, würde sie normalerweise die C++-Werte true
und false
verwenden (und nicht ihre internen Darstellungen von JavaScript-true
und -false
).
Zusätzlich zu den Sprachtypen verwendet die Spezifikation auch Spezifikationstypen, die nur in der Spezifikation vorkommen, nicht jedoch in der JavaScript-Sprache. Die JavaScript-Engine muss diese nicht implementieren (kann dies aber). In diesem Blogbeitrag werden wir den Spezifikationstyp Record (und seinen Untertyp Completion Record) kennenlernen.
Abstrakte Operationen
Abstrakte Operationen sind Funktionen, die in der ECMAScript-Spezifikation definiert sind; sie dienen der Zweckmäßigkeit beim Verfassen der Spezifikation. Eine JavaScript-Engine muss sie nicht als separate Funktionen innerhalb der Engine implementieren. Sie können nicht direkt aus JavaScript aufgerufen werden.
Interne Slots und interne Methoden
Interne Slots und interne Methoden verwenden Namen, die in [[ ]]
eingeschlossen sind.
Interne Slots sind Datenmitglieder eines JavaScript-Objekts oder eines Spezifikationstyps. Sie werden zur Speicherung des Zustands des Objekts verwendet. Interne Methoden sind Mitgliederfunktionen eines JavaScript-Objekts.
Zum Beispiel hat jedes JavaScript-Objekt einen internen Slot [[Prototype]]
und eine interne Methode [[GetOwnProperty]]
.
Interne Slots und Methoden sind aus JavaScript nicht zugänglich. Zum Beispiel können Sie nicht auf o.[[Prototype]]
zugreifen oder o.[[GetOwnProperty]]()
aufrufen. Eine JavaScript-Engine kann sie für den eigenen internen Gebrauch implementieren, muss dies aber nicht.
Manchmal delegieren interne Methoden an ähnlich benannte abstrakte Operationen, wie im Fall der [[GetOwnProperty]]
-Methode gewöhnlicher Objekte:
Wenn die interne Methode
[[GetOwnProperty]]
vonO
mit dem EigenschaftsschlüsselP
aufgerufen wird, werden die folgenden Schritte ausgeführt:
- Rückgabe von
! OrdinaryGetOwnProperty(O, P)
.
(Wir werden im nächsten Kapitel herausfinden, was das Ausrufezeichen bedeutet.)
OrdinaryGetOwnProperty
ist keine interne Methode, da sie nicht mit einem Objekt verknüpft ist; stattdessen wird das Objekt, auf dem sie arbeitet, als Parameter übergeben.
OrdinaryGetOwnProperty
wird „ordinary“ (gewöhnlich) genannt, da sie auf gewöhnlichen Objekten arbeitet. ECMAScript-Objekte können entweder ordinary (gewöhnlich) oder exotic (exotisch) sein. Gewöhnliche Objekte müssen das Standardverhalten für eine Reihe von Methoden namens essential internal methods (wesentliche interne Methoden) aufweisen. Wenn ein Objekt vom Standardverhalten abweicht, ist es exotisch.
Das bekannteste exotische Objekt ist das Array
, da seine Eigenschaft length
auf nicht standardmäßige Weise funktioniert: Das Festlegen der Eigenschaft length
kann Elemente aus dem Array
entfernen.
Wesentliche interne Methoden sind die hier aufgeführten Methoden hier.
Completion Records
Was ist mit den Fragezeichen und Ausrufezeichen? Um sie zu verstehen, müssen wir uns mit Completion Records befassen!
Ein Completion Record ist ein Spezifikationstyp (nur für Spezifikationszwecke definiert). Eine JavaScript-Engine muss keinen entsprechenden internen Datentyp haben.
Ein Completion Record ist ein „record“ — ein Datentyp mit einer festen Reihe benannter Felder. Ein Completion Record hat drei Felder:
Name | Beschreibung |
---|---|
[[Type]] | Eines von: normal , break , continue , return , oder throw . Alle anderen Typen außer normal sind abrupt completions (abrupte Abschlüsse). |
[[Value]] | Der Wert, der erzeugt wurde, als der Abschluss eingetreten ist, z. B. der Rückgabewert einer Funktion oder die Ausnahme (falls eine ausgelöst wurde). |
[[Target]] | Wird für gerichtete Kontrollübertragungen verwendet (nicht relevant für diesen Blog-Post). |
Jede abstrakte Operation gibt implizit einen Completion Record zurück. Selbst wenn es so aussieht, als würde eine abstrakte Operation einen einfachen Typ wie Boolean zurückgeben, wird dieser implizit in einen Completion Record mit dem Typ normal
eingebettet (siehe Implicit Completion Values).
Hinweis 1: Die Spezifikation ist in dieser Hinsicht nicht vollständig konsistent; es gibt einige Hilfsfunktionen, die reine Werte zurückgeben und deren Rückgabewerte unverändert verwendet werden, ohne den Wert aus dem Completion Record zu extrahieren. Dies ist meist aus dem Kontext ersichtlich.
Hinweis 2: Die Spezifikationsbearbeiter untersuchen, ob der Umgang mit Completion Records expliziter gestaltet werden kann.
Wenn ein Algorithmus eine Ausnahme auslöst, bedeutet das, dass ein Completion Record mit [[Type]]
throw
zurückgegeben wird, dessen [[Value]]
das Ausnahmeobjekt ist. Wir ignorieren vorerst die Typen break
, continue
und return
.
ReturnIfAbrupt(argument)
bedeutet, dass die folgenden Schritte ausgeführt werden:
- Wenn
argument
abrupt ist, geben Sieargument
zurück.- Setzen Sie
argument
aufargument.[[Value]]
.
Das heißt, wir prüfen einen Completion Record; wenn es ein abrupter Abschluss ist, geben wir sofort zurück. Andernfalls extrahieren wir den Wert aus dem Completion Record.
ReturnIfAbrupt
sieht möglicherweise wie ein Funktionsaufruf aus, ist es aber nicht. Es bewirkt, dass die Funktion, in der ReturnIfAbrupt()
vorkommt, zurückgegeben wird, nicht die Funktion ReturnIfAbrupt
selbst. Es verhält sich eher wie ein Makro in C-ähnlichen Sprachen.
ReturnIfAbrupt
kann wie folgt verwendet werden:
- Lassen Sie
obj
Foo()
sein. (obj
ist ein Completion Record.)ReturnIfAbrupt(obj)
.Bar(obj)
. (Falls wir noch hier sind, istobj
der extrahierte Wert aus dem Completion Record.)
Und jetzt kommt das Fragezeichen ins Spiel: ? Foo()
ist gleichbedeutend mit ReturnIfAbrupt(Foo())
. Die Verwendung einer Abkürzung ist praktisch: Wir müssen den Fehlerbehandlungscode nicht jedes Mal explizit schreiben.
Ebenso ist Lassen Sie val ! Foo()
gleichbedeutend mit:
- Lassen Sie
val
Foo()
sein.- Behauptung:
val
ist kein abrupter Abschluss.- Setzen Sie
val
aufval.[[Value]]
.
Mit diesem Wissen können wir Object.prototype.hasOwnProperty
wie folgt neu schreiben:
Object.prototype.hasOwnProperty(V)
- Lass
P
den Wert vonToPropertyKey(V)
sein.- Wenn
P
eine abrupte Beendigung ist, gibP
zurück.- Setze
P
aufP.[[Value]]
.- Lass
O
den Wert vonToObject(this value)
sein.- Wenn
O
eine abrupte Beendigung ist, gibO
zurück.- Setze
O
aufO.[[Value]]
.- Lass
temp
den Wert vonHasOwnProperty(O, P)
sein.- Wenn
temp
eine abrupte Beendigung ist, gibtemp
zurück.- Setze
temp
auftemp.[[Value]]
.- Gib
NormalCompletion(temp)
zurück.
…und wir können HasOwnProperty
so umschreiben:
HasOwnProperty(O, P)
- Stelle sicher:
Type(O)
istObject
.- Stelle sicher:
IsPropertyKey(P)
isttrue
.- Lass
desc
den Wert vonO.[[GetOwnProperty]](P)
sein.- Wenn
desc
eine abrupte Beendigung ist, gibdesc
zurück.- Setze
desc
aufdesc.[[Value]]
.- Wenn
desc
undefined
ist, gibNormalCompletion(false)
zurück.- Gib
NormalCompletion(true)
zurück.
Wir können auch die interne Methode [[GetOwnProperty]]
ohne das Ausrufezeichen umschreiben:
O.[[GetOwnProperty]]
- Lass
temp
den Wert vonOrdinaryGetOwnProperty(O, P)
sein.- Stelle sicher:
temp
ist keine abrupte Beendigung.- Setze
temp
auftemp.[[Value]]
.- Gib
NormalCompletion(temp)
zurück.
Hier nehmen wir an, dass temp
eine brandneue temporäre Variable ist, die mit nichts anderem kollidiert.
Wir haben auch das Wissen genutzt, dass wenn eine Return-Anweisung etwas anderes als einen Completion Record zurückgibt, es implizit in einen NormalCompletion
eingeschlossen wird.
Nebenschauplatz: Return ? Foo()
Die Spezifikation verwendet die Notation Return ? Foo()
— warum das Fragezeichen?
Return ? Foo()
erweitert sich zu:
- Lass
temp
den Wert vonFoo()
sein.- Wenn
temp
eine abrupte Beendigung ist, gibtemp
zurück.- Setze
temp
auftemp.[[Value]]
.- Gib
NormalCompletion(temp)
zurück.
Was dasselbe ist wie Return Foo()
; es verhält sich auf die gleiche Weise sowohl für abrupte als auch normale Beendigungen.
Return ? Foo()
wird nur aus redaktionellen Gründen verwendet, um deutlicher zu machen, dass Foo
einen Completion Record zurückgibt.
Assertions
Assertions in der Spezifikation stellen die Invarianzbedingungen der Algorithmen sicher. Sie sind zur Klarstellung hinzugefügt, fügen jedoch keine Anforderungen an die Implementierung hinzu — die Implementierung muss sie nicht prüfen.
Weiter geht's
Die abstrakten Operationen delegieren an andere abstrakte Operationen (siehe Bild unten), aber basierend auf diesem Blogbeitrag sollten wir in der Lage sein, herauszufinden, was sie tun. Wir werden auf Property Descriptors stoßen, die einfach ein weiterer Spezifikationstyp sind.
Zusammenfassung
Wir haben eine einfache Methode — Object.prototype.hasOwnProperty
— und die abstrakten Operationen gelesen, die sie aufruft. Wir haben uns mit den Abkürzungen ?
und !
, die sich auf die Fehlerbehandlung beziehen, vertraut gemacht. Wir sind auf Spezifikationstypen, interne Slots und interne Methoden gestoßen.
Nützliche Links
Wie man die ECMAScript-Spezifikation liest: ein Tutorial, das einen Großteil des in diesem Beitrag behandelten Materials aus einem etwas anderen Blickwinkel abdeckt.