Kapitel 28. Unterklassen von Built-ins
Inhaltsverzeichnis
Das Buch kaufen
(Werbung, bitte nicht blockieren.)

Kapitel 28. Unterklassen von Built-ins

JavaScript’s eingebaute Konstruktoren sind schwierig zu unterklassifizieren. Dieses Kapitel erklärt warum und präsentiert Lösungen.

Terminologie

Wir verwenden den Ausdruck einen Built-in unterklassifizieren und vermeiden den Begriff erweitern, da dieser in JavaScript bereits vergeben ist.

Unterklassifizierung eines Built-ins A
Erstellen eines Unterkonstruktors B eines gegebenen Built-in-Konstruktors A. Instanzen von B sind auch Instanzen von A.
Erweitern eines Objekts obj
Kopieren der Eigenschaften eines Objekts auf ein anderes. Underscore.js verwendet diesen Begriff und setzt eine Tradition fort, die vom Prototype-Framework etabliert wurde.

Es gibt zwei Hindernisse für die Unterklassifizierung eines Built-ins: Instanzen mit internen Eigenschaften und ein Konstruktor, der nicht als Funktion aufgerufen werden kann.

Hindernis 1: Instanzen mit internen Eigenschaften

Die meisten Built-in-Konstruktoren haben Instanzen mit sogenannten internen Eigenschaften (siehe Arten von Eigenschaften), deren Namen in doppelten eckigen Klammern geschrieben sind, wie folgt: [[PrimitiveValue]]. Interne Eigenschaften werden von der JavaScript-Engine verwaltet und sind normalerweise nicht direkt in JavaScript zugänglich. Die normale Unterklassifizierungstechnik in JavaScript besteht darin, einen Superkonstruktor als Funktion mit dem this des Unterkonstruktors aufzurufen (siehe Ebene 4: Vererbung zwischen Konstruktoren).

function Super(x, y) {
    this.x = x;  // (1)
    this.y = y;  // (1)
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (2)
    // Add subproperty
    this.z = z;
}

Die meisten Built-ins ignorieren die als this übergebene Unterinstanz (2), ein Hindernis, das im nächsten Abschnitt beschrieben wird. Darüber hinaus ist das Hinzufügen interner Eigenschaften zu einer bestehenden Instanz (1) im Allgemeinen unmöglich, da sie die Natur der Instanz grundlegend verändern. Daher kann der Aufruf unter (2) nicht zum Hinzufügen interner Eigenschaften verwendet werden. Die folgenden Konstruktoren haben Instanzen mit internen Eigenschaften:

Wrapper-Konstruktoren

Instanzen von Boolean, Number und String umschließen primitive Werte. Sie alle haben die interne Eigenschaft [[PrimitiveValue]], deren Wert von valueOf() zurückgegeben wird; String hat zwei zusätzliche Instanzeigenschaften.

  • Boolean: Interne Instanzeigenschaft [[PrimitiveValue]].
  • Number: Interne Instanzeigenschaft [[PrimitiveValue]].
  • String: Interne Instanzeigenschaft [[PrimitiveValue]], benutzerdefinierte interne Instanzmethode [[GetOwnProperty]], normale Instanzeigenschaft length. [[GetOwnProperty]] ermöglicht den indizierten Zugriff auf Zeichen durch Lesen des umschlossenen Strings, wenn ein Array-Index verwendet wird.
Array
Die benutzerdefinierte interne Instanzmethode [[DefineOwnProperty]] fängt das Setzen von Eigenschaften ab. Sie stellt sicher, dass die length-Eigenschaft korrekt funktioniert, indem sie length auf dem neuesten Stand hält, wenn Array-Elemente hinzugefügt werden, und überschüssige Elemente entfernt, wenn length verkleinert wird.
Datum
Die interne Instanzeigenschaft [[PrimitiveValue]] speichert die von einer Datumsinstanz dargestellte Zeit (als Anzahl der Millisekunden seit dem 1. Januar 1970 00:00:00 UTC).
Funktion
Die interne Instanzeigenschaft [[Call]] (der Code, der ausgeführt wird, wenn eine Instanz aufgerufen wird) und möglicherweise andere.
RegExp

Die interne Instanzeigenschaft [[Match]], plus zwei nicht-interne Instanzeigenschaften. Gemäß der ECMAScript-Spezifikation:

Der Wert der internen Eigenschaft [[Match]] ist eine implementierungsabhängige Darstellung des Musters des RegExp-Objekts.

Die einzigen Built-in-Konstruktoren, die keine internen Eigenschaften haben, sind Error und Object.

Workaround für Hindernis 1

MyArray ist eine Unterklasse von Array. Sie hat einen Getter size, der die tatsächlichen Elemente in einem Array zurückgibt und Lücken ignoriert (wo length Lücken berücksichtigt). Der Trick, der zur Implementierung von MyArray verwendet wird, besteht darin, dass sie eine Array-Instanz erstellt und ihre Methoden hineinkopiert:[22]

function MyArray(/*arguments*/) {
    var arr = [];
    // Don’t use Array constructor to set up elements (doesn’t always work)
    Array.prototype.push.apply(arr, arguments);  // (1)
    copyOwnPropertiesFrom(arr, MyArray.methods);
    return arr;
}
MyArray.methods = {
    get size() {
        var size = 0;
        for (var i=0; i < this.length; i++) {
            if (i in this) size++;
        }
        return size;
    }
}

Dieser Code verwendet die Hilfsfunktion copyOwnPropertiesFrom(), die in Kopieren eines Objekts gezeigt und erklärt wird.

Wir rufen den Array-Konstruktor in Zeile (1) nicht auf, wegen einer Eigenart: Wenn er mit einem einzelnen Parameter aufgerufen wird, der eine Zahl ist, wird die Zahl kein Element, sondern bestimmt die Länge eines leeren Arrays (siehe Initialisieren eines Arrays mit Elementen (vermeiden!)).

Hier ist die Interaktion

> var a = new MyArray('a', 'b')
> a.length = 4;
> a.length
4
> a.size
2

Vorbehalte

Das Kopieren von Methoden auf eine Instanz führt zu Redundanzen, die mit einem Prototyp vermieden werden könnten (wenn wir die Option hätten, einen zu verwenden). Zusätzlich erstellt MyArray Objekte, die keine Instanzen davon sind.

> a instanceof MyArray
false
> a instanceof Array
true

Hindernis 2: Ein Konstruktor, der nicht als Funktion aufgerufen werden kann

Auch wenn Error und Unterklassen keine Instanzen mit internen Eigenschaften haben, können Sie sie immer noch nicht einfach unterklassifizieren, da das Standardmuster für die Unterklassifizierung nicht funktioniert (wiederholt von früher):

function Super(x, y) {
    this.x = x;
    this.y = y;
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (1)
    // Add subproperty
    this.z = z;
}

Das Problem ist, dass Error immer eine neue Instanz erzeugt, auch wenn es als Funktion aufgerufen wird (1); das heißt, es ignoriert den Parameter this, der ihm über call() übergeben wird.

> var e = {};
> Object.getOwnPropertyNames(Error.call(e)) // new instance
[ 'stack', 'arguments', 'type' ]
> Object.getOwnPropertyNames(e) // unchanged
[]

In der vorherigen Interaktion gibt Error eine Instanz mit eigenen Eigenschaften zurück, aber es ist eine neue Instanz, nicht e. Das Unterklassifizierungsmuster würde nur funktionieren, wenn Error die eigenen Eigenschaften zu this (e im vorherigen Fall) hinzufügen würde.

Workaround für Hindernis 2

Erstellen Sie innerhalb des Unterkonstruktors eine neue Superinstanz und kopieren Sie deren eigene Eigenschaften auf die Unterinstanz.

function MyError() {
    // Use Error as a function
    var superInstance = Error.apply(null, arguments);
    copyOwnPropertiesFrom(this, superInstance);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

Die Hilfsfunktion copyOwnPropertiesFrom() ist in Kopieren eines Objekts gezeigt. Ausprobieren von MyError

try {
    throw new MyError('Something happened');
} catch (e) {
    console.log('Properties: '+Object.getOwnPropertyNames(e));
}

hier ist die Ausgabe unter Node.js:

Properties: stack,arguments,message,type

Die instanceof-Beziehung ist wie sie sein sollte.

> new MyError() instanceof Error
true
> new MyError() instanceof MyError
true


[22] Inspiriert von einem Blogbeitrag von Ben Nadel.

Weiter: 29. JSDoc: Generieren von API-Dokumentation