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
hasOwnPropertymit dem ArgumentVaufgerufen wird, werden die folgenden Schritte ausgeführt:
- Setze
Pauf? ToPropertyKey(V).- Setze
Oauf? ToObject(this value).- Gib
? HasOwnProperty(O, P)zurück.
…und…
Die abstrakte Operation
HasOwnPropertydient 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 ArgumentenOundPaufgerufen, wobeiOdas Objekt undPder Eigenschaftsschlüssel ist. Diese abstrakte Operation führt die folgenden Schritte aus:
- Behauptung:
Type(O)istObject.- Behauptung:
IsPropertyKey(P)isttrue.- Setze
descauf? O.[[GetOwnProperty]](P).- Wenn
descundefinedist, gibfalsezurück.- Gib
truezurü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]]vonOmit dem EigenschaftsschlüsselPaufgerufen 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
argumentabrupt ist, geben Sieargumentzurück.- Setzen Sie
argumentaufargument.[[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
objFoo()sein. (objist ein Completion Record.)ReturnIfAbrupt(obj).Bar(obj). (Falls wir noch hier sind, istobjder 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
valFoo()sein.- Behauptung:
valist kein abrupter Abschluss.- Setzen Sie
valaufval.[[Value]].
Mit diesem Wissen können wir Object.prototype.hasOwnProperty wie folgt neu schreiben:
Object.prototype.hasOwnProperty(V)
- Lass
Pden Wert vonToPropertyKey(V)sein.- Wenn
Peine abrupte Beendigung ist, gibPzurück.- Setze
PaufP.[[Value]].- Lass
Oden Wert vonToObject(this value)sein.- Wenn
Oeine abrupte Beendigung ist, gibOzurück.- Setze
OaufO.[[Value]].- Lass
tempden Wert vonHasOwnProperty(O, P)sein.- Wenn
tempeine abrupte Beendigung ist, gibtempzurück.- Setze
tempauftemp.[[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
descden Wert vonO.[[GetOwnProperty]](P)sein.- Wenn
desceine abrupte Beendigung ist, gibdesczurück.- Setze
descaufdesc.[[Value]].- Wenn
descundefinedist, gibNormalCompletion(false)zurück.- Gib
NormalCompletion(true)zurück.
Wir können auch die interne Methode [[GetOwnProperty]] ohne das Ausrufezeichen umschreiben:
O.[[GetOwnProperty]]
- Lass
tempden Wert vonOrdinaryGetOwnProperty(O, P)sein.- Stelle sicher:
tempist keine abrupte Beendigung.- Setze
tempauftemp.[[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
tempden Wert vonFoo()sein.- Wenn
tempeine abrupte Beendigung ist, gibtempzurück.- Setze
tempauftemp.[[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.