Kapitel 17. Objekte und Vererbung
Inhaltsverzeichnis
Das Buch kaufen
(Werbung, bitte nicht blockieren.)

Kapitel 17. Objekte und Vererbung

Es gibt mehrere Ebenen der objektorientierten Programmierung (OOP) in JavaScript:

Jede neue Ebene hängt nur von den vorherigen ab, was es Ihnen ermöglicht, JavaScript OOP inkrementell zu lernen. Ebenen 1 und 2 bilden einen einfachen Kern, auf den Sie zurückgreifen können, wann immer Sie von den komplizierteren Ebenen 3 und 4 verwirrt sind.

Ebene 1: Einzelne Objekte

Grob gesagt sind alle Objekte in JavaScript Maps (Wörterbücher) von Strings zu Werten. Ein (Schlüssel, Wert)-Eintrag in einem Objekt wird als Eigenschaft bezeichnet. Der Schlüssel einer Eigenschaft ist immer ein Textstring. Der Wert einer Eigenschaft kann jeder JavaScript-Wert sein, einschließlich einer Funktion. Methoden sind Eigenschaften, deren Werte Funktionen sind.

Objektliterale

JavaScript- Objektliterale erlauben Ihnen, direkt einfache Objekte (direkte Instanzen von Object) zu erstellen. Der folgende Code verwendet ein Objektliteral, um ein Objekt der Variablen jane zuzuweisen. Das Objekt hat die beiden Eigenschaften: name und describe. describe ist eine Methode:

var jane = {
    name: 'Jane',

    describe: function () {
        return 'Person named '+this.name;  // (1)
    },  // (2)
};
  1. Verwenden Sie this in Methoden, um auf das aktuelle Objekt zu verweisen (auch als Empfänger eines Methodenaufrufs bezeichnet).
  2. ECMAScript 5 erlaubt ein nachgestelltes Komma (nach der letzten Eigenschaft) in einem Objektliteral. Leider unterstützen nicht alle älteren Browser dies. Ein nachgestelltes Komma ist nützlich, da Sie Eigenschaften neu anordnen können, ohne sich darum kümmern zu müssen, welche Eigenschaft die letzte ist.

Sie könnten den Eindruck bekommen, dass Objekte nur Maps von Strings zu Werten sind. Aber sie sind mehr als das: sie sind echte Allzweckobjekte. Zum Beispiel können Sie Vererbung zwischen Objekten verwenden (siehe Ebene 2: Die Prototyp-Beziehung zwischen Objekten) und Sie können Objekte vor Änderungen schützen. Die Möglichkeit, Objekte direkt zu erstellen, ist eine der herausragenden Eigenschaften von JavaScript: Sie können mit konkreten Objekten beginnen (keine Klassen benötigt!) und Abstraktionen später einführen. Zum Beispiel sind Konstruktoren, die Fabriken für Objekte sind (wie in Ebene 3: Konstruktoren – Fabriken für Instanzen besprochen), grob ähnlich wie Klassen in anderen Sprachen.

Punktoperator (.): Zugriff auf Eigenschaften über feste Schlüssel

Der Punktoperator bietet eine kompakte Syntax für den Zugriff auf Eigenschaften. Die Eigenschaftsschlüssel müssen Bezeichner sein (siehe Gültige Bezeichner). Wenn Sie Eigenschaften mit beliebigen Namen lesen oder schreiben möchten, müssen Sie den Klammeroperator verwenden (siehe Klammeroperator ([]): Zugriff auf Eigenschaften über berechnete Schlüssel).

Die Beispiele in diesem Abschnitt arbeiten mit dem folgenden Objekt

var jane = {
    name: 'Jane',

    describe: function () {
        return 'Person named '+this.name;
    }
};

Abrufen von Eigenschaften

Der Punktoperator ermöglicht es Ihnen, eine Eigenschaft abzurufen (ihren Wert zu lesen). Hier sind einige Beispiele

> jane.name  // get property `name`
'Jane'
> jane.describe  // get property `describe`
[Function]

Das Abrufen einer nicht existierenden Eigenschaft gibt undefined zurück

> jane.unknownProperty
undefined

Aufrufen von Methoden

Der Punktoperator wird auch verwendet, um Methoden aufzurufen:

> jane.describe()  // call method `describe`
'Person named Jane'

Festlegen von Eigenschaften

Sie können den Zuweisungsoperator (=) verwenden, um den Wert einer Eigenschaft festzulegen, auf die über die Punktnotation verwiesen wird. Zum Beispiel:

> jane.name = 'John';  // set property `name`
> jane.describe()
'Person named John'

Wenn eine Eigenschaft noch nicht existiert, wird sie beim Festlegen automatisch erstellt. Wenn eine Eigenschaft bereits existiert, ändert das Festlegen ihren Wert.

Der Rückgabewert von delete

delete gibt false zurück, wenn die Eigenschaft eine eigene Eigenschaft ist, aber nicht gelöscht werden kann. In allen anderen Fällen gibt es true zurück. Hier sind einige Beispiele.

Zur Vorbereitung erstellen wir eine löschbare Eigenschaft und eine weitere, die nicht gelöscht werden kann (Abrufen und Definieren von Eigenschaften über Deskriptoren erklärt Object.defineProperty())

var obj = {};
Object.defineProperty(obj, 'canBeDeleted', {
    value: 123,
    configurable: true
});
Object.defineProperty(obj, 'cannotBeDeleted', {
    value: 456,
    configurable: false
});

delete gibt false zurück für eigene Eigenschaften, die nicht gelöscht werden können

> delete obj.cannotBeDeleted
false

delete gibt true in allen anderen Fällen zurück

> delete obj.doesNotExist
true
> delete obj.canBeDeleted
true

delete gibt true zurück, auch wenn es nichts ändert (geerbte Eigenschaften werden nie entfernt)

> delete obj.toString
true
> obj.toString // still there
[Function: toString]

Ungewöhnliche Eigenschaftsschlüssel

Während Sie reservierte Wörter (wie var und function) nicht als Variablennamen verwenden können, können Sie sie als Eigenschaftsschlüssel verwenden:

> var obj = { var: 'a', function: 'b' };
> obj.var
'a'
> obj.function
'b'

Zahlen können als Eigenschaftsschlüssel in Objektliteralen verwendet werden, werden aber als Strings interpretiert. Der Punktoperator kann nur auf Eigenschaften zugreifen, deren Schlüssel Bezeichner sind. Daher benötigen Sie den Klammeroperator (im folgenden Beispiel gezeigt), um auf Eigenschaften zuzugreifen, deren Schlüssel Zahlen sind:

> var obj = { 0.7: 'abc' };
> Object.keys(obj)
[ '0.7' ]
> obj['0.7']
'abc'

Objektliterale erlauben Ihnen auch, beliebige Strings (die weder Bezeichner noch Zahlen sind) als Eigenschaftsschlüssel zu verwenden, aber Sie müssen sie in Anführungszeichen setzen. Auch hier benötigen Sie den Klammeroperator, um auf die Eigenschaftswerte zuzugreifen

> var obj = { 'not an identifier': 123 };
> Object.keys(obj)
[ 'not an identifier' ]
> obj['not an identifier']
123

Klammeroperator ([]): Zugriff auf Eigenschaften über berechnete Schlüssel

Während der Punktoperator mit festen Eigenschaftsschlüsseln funktioniert, ermöglicht Ihnen der Klammeroperator, eine Eigenschaft über einen Ausdruck anzusprechen.

Abrufen von Eigenschaften über den Klammeroperator

Der Klammeroperator lässt Sie den Schlüssel einer Eigenschaft über einen Ausdruck berechnen:

> var obj = { someProperty: 'abc' };

> obj['some' + 'Property']
'abc'

> var propKey = 'someProperty';
> obj[propKey]
'abc'

Das ermöglicht auch den Zugriff auf Eigenschaften, deren Schlüssel keine Bezeichner sind

> var obj = { 'not an identifier': 123 };
> obj['not an identifier']
123

Beachten Sie, dass der Klammeroperator seinen Inhalt zu einem String koerziert. Zum Beispiel

> var obj = { '6': 'bar' };
> obj[3+3]  // key: the string '6'
'bar'

Aufrufen von Methoden über den Klammeroperator

Das Aufrufen von Methoden funktioniert wie erwartet:

> var obj = { myMethod: function () { return true } };
> obj['myMethod']()
true

Löschen von Eigenschaften über den Klammeroperator

Das Löschen von Eigenschaften funktioniert ebenfalls ähnlich wie beim Punktoperator:

> var obj = { 'not an identifier': 1, prop: 2 };
> Object.keys(obj)
[ 'not an identifier', 'prop' ]
> delete obj['not an identifier']
true
> Object.keys(obj)
[ 'prop' ]

Wenn Sie eine Funktion aufrufen, ist this immer ein (impliziter) Parameter:

Normale Funktionen im sloppy mode

Auch wenn normale Funktionen keinen Nutzen für this haben, existiert es dennoch als spezielle Variable, deren Wert immer das globale Objekt ist (window in Browsern; siehe Das globale Objekt)

> function returnThisSloppy() { return this }
> returnThisSloppy() === window
true
Normale Funktionen im strict mode

this ist immer undefined

> function returnThisStrict() { 'use strict'; return this }
> returnThisStrict() === undefined
true
Methoden

this bezieht sich auf das Objekt, auf dem die Methode aufgerufen wurde

> var obj = { method: returnThisStrict };
> obj.method() === obj
true

Bei Methoden wird der Wert von this als Empfänger des Methodenaufrufs bezeichnet.

Funktionen unter expliziter Festlegung von this aufrufen: call(), apply() und bind()

Denken Sie daran, dass Funktionen auch Objekte sind. Daher hat jede Funktion eigene Methoden. Drei davon werden in diesem Abschnitt vorgestellt und helfen beim Aufrufen von Funktionen. Diese drei Methoden werden in den folgenden Abschnitten verwendet, um einige der Fallstricke beim Aufrufen von Funktionen zu umgehen. Die kommenden Beispiele beziehen sich alle auf das folgende Objekt, jane:

var jane = {
    name: 'Jane',
    sayHelloTo: function (otherName) {
        'use strict';
        console.log(this.name+' says hello to '+otherName);
    }
};

apply() für Konstruktoren

Nehmen wir an, JavaScript hätte einen Dreipunktoperator (...), der Arrays in tatsächliche Parameter umwandelt. Ein solcher Operator würde es Ihnen ermöglichen, Math.max() (siehe Andere Funktionen) mit Arrays zu verwenden. In diesem Fall wären die folgenden beiden Ausdrücke äquivalent

Math.max(...[13, 7, 30])
Math.max(13, 7, 30)

Für Funktionen können Sie den Effekt des Dreipunktoperators über apply() erzielen

> Math.max.apply(null, [13, 7, 30])
30

Der Dreipunktoperator wäre auch für Konstruktoren sinnvoll

new Date(...[2011, 11, 24]) // Christmas Eve 2011

Leider funktioniert apply() hier nicht, da es nur bei Funktions- oder Methodenaufrufen hilft, nicht bei Konstruktoraufrufen.

Manuelles Simulieren von apply() für Konstruktoren

Wir können apply() in zwei Schritten simulieren.

Schritt 1

Übergabe der Argumente an Date über einen Methodenaufruf (sie sind noch nicht in einem Array)

new (Date.bind(null, 2011, 11, 24))

Der vorherige Code verwendet bind(), um einen Konstruktor ohne Parameter zu erstellen und ihn über new aufzurufen.

Schritt 2

Verwenden Sie apply(), um ein Array an bind() zu übergeben. Da bind() ein Methodenaufruf ist, können wir apply() verwenden

new (Function.prototype.bind.apply(
         Date, [null, 2011, 11, 24]))

Das vorherige Array enthält null, gefolgt von den Elementen von arr. Wir können concat() verwenden, um es zu erstellen, indem wir null voranstellen zu arr

var arr = [2011, 11, 24];
new (Function.prototype.bind.apply(
         Date, [null].concat(arr)))

Eine Bibliotheksmethode

Die vorherige manuelle Umgehung ist von einer Bibliotheksmethode von Mozilla inspiriert. Das Folgende ist eine leicht bearbeitete Version davon

if (!Function.prototype.construct) {
    Function.prototype.construct = function(argArray) {
        if (! Array.isArray(argArray)) {
            throw new TypeError("Argument must be an array");
        }
        var constr = this;
        var nullaryFunc = Function.prototype.bind.apply(
            constr, [null].concat(argArray));
        return new nullaryFunc();
    };
}

Hier ist die Methode in Gebrauch

> Date.construct([2011, 11, 24])
Sat Dec 24 2011 00:00:00 GMT+0100 (CET)

Ein alternativer Ansatz

Eine Alternative zum vorherigen Ansatz ist es, eine nicht initialisierte Instanz über Object.create() zu erstellen und dann den Konstruktor (als Funktion) über apply() aufzurufen. Das bedeutet, dass Sie effektiv den new-Operator neu implementieren (einige Prüfungen werden weggelassen)

Function.prototype.construct = function(argArray) {
    var constr = this;
    var inst = Object.create(constr.prototype);
    var result = constr.apply(inst, argArray); // (1)

    // Check: did the constructor return an object
    // and prevent `this` from being the result?
    return result ? result : inst;
};

Warnung

Der vorherige Code funktioniert nicht für die meisten eingebauten Konstruktoren, die immer neue Instanzen erzeugen, wenn sie als Funktionen aufgerufen werden. Das heißt, Schritt (1) richtet inst nicht wie gewünscht ein.

Fallstrick: this verlieren, wenn eine Methode extrahiert wird

Wenn Sie eine Methode aus einem Objekt extrahieren, wird sie wieder zu einer echten Funktion. Ihre Verbindung zum Objekt ist getrennt, und sie funktioniert normalerweise nicht mehr richtig. Nehmen wir zum Beispiel das folgende Objekt, counter:

var counter = {
    count: 0,
    inc: function () {
        this.count++;
    }
}

Das Extrahieren von inc und dessen Aufruf (als Funktion!) schlägt fehl

> var func = counter.inc;
> func()
> counter.count  // didn’t work
0

Hier ist die Erklärung: Wir haben den Wert von counter.inc als Funktion aufgerufen. Daher ist this das globale Objekt und wir haben window.count++ ausgeführt. window.count existiert nicht und ist undefined. Das Anwenden des Operators ++ darauf setzt ihn auf NaN

> count  // global variable
NaN

Sie verschachteln häufig Funktionsdefinitionen in JavaScript, da Funktionen Parameter sein können (z. B. Callbacks) und da sie an Ort und Stelle erstellt werden können, über Funktionsausdrücke. Dies birgt ein Problem, wenn eine Methode eine normale Funktion enthält und Sie das this der ersteren innerhalb der letzteren zugreifen möchten, da das this der Methode von this der normalen Funktion überschattet wird (die nicht einmal einen eigenen this benötigt). Im folgenden Beispiel versucht die Funktion bei (1), das this der Methode bei (2) zu erreichen:

var obj = {
    name: 'Jane',
    friends: [ 'Tarzan', 'Cheeta' ],
    loop: function () {
        'use strict';
        this.friends.forEach(
            function (friend) {  // (1)
                console.log(this.name+' knows '+friend);  // (2)
            }
        );
    }
};

Dies schlägt fehl, da die Funktion bei (1) ihr eigenes this hat, das hier undefined ist

> obj.loop();
TypeError: Cannot read property 'name' of undefined

Es gibt drei Möglichkeiten, dieses Problem zu umgehen.

Wir können bind() verwenden, um dem Callback einen festen Wert für this zu geben – nämlich das this der Methode (Zeile (1)):

loop: function () {
    'use strict';
    this.friends.forEach(function (friend) {
        console.log(this.name+' knows '+friend);
    }.bind(this));  // (1)
}

Umgehung 3: a thisValue für forEach()

Eine Umgehung, die spezifisch für forEach() ist (siehe Untersuchungsmethoden), ist, nach dem Callback einen zweiten Parameter anzugeben, der zu dem this des Callbacks wird:

loop: function () {
    'use strict';
    this.friends.forEach(function (friend) {
        console.log(this.name+' knows '+friend);
    }, this);
}

Die Prototyp-Beziehung zwischen zwei Objekten betrifft die Vererbung: jedes Objekt kann ein anderes Objekt als seinen Prototyp haben. Dann erbt das erstere Objekt alle Eigenschaften seines Prototyps. Ein Objekt gibt seinen Prototyp über die interne Eigenschaft [[Prototype]] an. Jedes Objekt hat diese Eigenschaft, aber sie kann null sein. Die Kette von Objekten, die durch die [[Prototype]]-Eigenschaft verbunden sind, wird als Prototyp-Kette bezeichnet (Abbildung 17-1).

Um zu sehen, wie die Prototyp-basierte (oder prototypische) Vererbung funktioniert, betrachten wir ein Beispiel (mit erfundener Syntax zur Angabe der [[Prototype]]-Eigenschaft)

var proto = {
    describe: function () {
        return 'name: '+this.name;
    }
};
var obj = {
    [[Prototype]]: proto,
    name: 'obj'
};

Das Objekt obj erbt die Eigenschaft describe von proto. Es hat auch eine sogenannte eigene (nicht vererbte, direkte) Eigenschaft, name.

Prototypen sind hervorragend zum Teilen von Daten zwischen Objekten geeignet: mehrere Objekte erhalten denselben Prototyp, der alle gemeinsamen Eigenschaften enthält. Betrachten wir ein Beispiel. Die Objekte jane und tarzan enthalten beide dieselbe Methode, describe(). Das ist etwas, das wir durch Teilen vermeiden möchten:

var jane = {
    name: 'Jane',
    describe: function () {
        return 'Person named '+this.name;
    }
};
var tarzan = {
    name: 'Tarzan',
    describe: function () {
        return 'Person named '+this.name;
    }
};

Beide Objekte sind Personen. Ihre name-Eigenschaft ist unterschiedlich, aber wir könnten sie die Methode describe teilen lassen. Das tun wir, indem wir einen gemeinsamen Prototyp namens PersonProto erstellen und describe darin platzieren (Abbildung 17-2).

Der folgende Code erstellt die Objekte jane und tarzan, die sich den Prototyp PersonProto teilen

var PersonProto = {
    describe: function () {
        return 'Person named '+this.name;
    }
};
var jane = {
    [[Prototype]]: PersonProto,
    name: 'Jane'
};
var tarzan = {
    [[Prototype]]: PersonProto,
    name: 'Tarzan'
};

Und hier ist die Interaktion

> jane.describe()
Person named Jane
> tarzan.describe()
Person named Tarzan

Dies ist ein übliches Muster: die Daten befinden sich im ersten Objekt einer Prototyp-Kette, während Methoden sich in späteren Objekten befinden. JavaScripts Art der prototypischen Vererbung ist darauf ausgelegt, dieses Muster zu unterstützen: das Festlegen einer Eigenschaft beeinflusst nur das erste Objekt in einer Prototyp-Kette, während das Abrufen einer Eigenschaft die gesamte Kette berücksichtigt (siehe Festlegen und Löschen beeinflusst nur eigene Eigenschaften).

Abrufen und Festlegen des Prototyps

Bisher haben wir so getan, als könnten wir auf die interne Eigenschaft [[Prototype]] aus JavaScript zugreifen. Aber die Sprache erlaubt Ihnen das nicht. Stattdessen gibt es Funktionen zum Lesen des Prototyps und zum Erstellen eines neuen Objekts mit einem gegebenen Prototyp.

Erstellen eines neuen Objekts mit einem gegebenen Prototyp

Dieser Aufruf:

Object.create(proto, propDescObj?)

erstellt ein Objekt, dessen Prototyp proto ist. Optional können Eigenschaften über Deskriptoren hinzugefügt werden (die in Eigenschaftsdeskriptoren erklärt werden). Im folgenden Beispiel erhält das Objekt jane den Prototyp PersonProto und eine veränderbare Eigenschaft name, deren Wert 'Jane' ist (wie über einen Eigenschaftsdeskriptor angegeben)

var PersonProto = {
    describe: function () {
        return 'Person named '+this.name;
    }
};
var jane = Object.create(PersonProto, {
    name: { value: 'Jane', writable: true }
});

Hier ist die Interaktion

> jane.describe()
'Person named Jane'

Aber Sie erstellen häufig nur ein leeres Objekt und fügen dann manuell Eigenschaften hinzu, da Deskriptoren umständlich sind

var jane = Object.create(PersonProto);
jane.name = 'Jane';

Lesen des Prototyps eines Objekts

Dieser Methodenaufruf:

Object.getPrototypeOf(obj)

gibt den Prototyp von obj zurück. Fortsetzung des vorherigen Beispiels

> Object.getPrototypeOf(jane) === PersonProto
true

Prüfen, ob ein Objekt ein Prototyp eines anderen ist

Diese Syntax:

Object.prototype.isPrototypeOf(obj)

prüft, ob der Empfänger der Methode ein (direkter oder indirekter) Prototyp von obj ist. Mit anderen Worten: befinden sich der Empfänger und obj in derselben Prototyp-Kette, und kommt obj vor dem Empfänger? Zum Beispiel

> var A = {};
> var B = Object.create(A);
> var C = Object.create(B);
> A.isPrototypeOf(C)
true
> C.isPrototypeOf(A)
false

Die spezielle Eigenschaft __proto__

Einige JavaScript-Engines haben eine spezielle Eigenschaft zum Abrufen und Festlegen des Prototyps eines Objekts: __proto__. Sie bringt direkten Zugriff auf [[Prototype]] in die Sprache:

> var obj = {};

> obj.__proto__ === Object.prototype
true

> obj.__proto__ = Array.prototype
> Object.getPrototypeOf(obj) === Array.prototype
true

Es gibt mehrere Dinge, die Sie über __proto__ wissen müssen

Festlegen und Löschen beeinflusst nur eigene Eigenschaften

Nur das Abrufen einer Eigenschaft berücksichtigt die vollständige Prototyp-Kette eines Objekts. Das Festlegen und Löschen ignoriert Vererbung und betrifft nur eigene Eigenschaften.

Ändern von Eigenschaften irgendwo in der Prototyp-Kette

Wenn Sie eine geerbte Eigenschaft ändern möchten, müssen Sie zuerst das Objekt finden, das sie besitzt (siehe Finden des Objekts, in dem eine Eigenschaft definiert ist) und dann die Änderung an diesem Objekt vornehmen. Zum Beispiel löschen wir die Eigenschaft foo aus dem vorherigen Beispiel

> delete getDefiningObject(obj, 'foo').foo;
true
> obj.foo
undefined

Operationen zum Iterieren über und Erkennen von Eigenschaften werden beeinflusst durch:

Sie können eigene Eigenschaftsschlüssel auflisten, alle aufzählbaren Eigenschaftsschlüssel auflisten und prüfen, ob eine Eigenschaft existiert. Die folgenden Unterabschnitte zeigen, wie.

Auflisten eigener Eigenschaftsschlüssel

Sie können entweder alle eigenen Eigenschaftsschlüssel auflisten oder nur die aufzählbaren:

Beachten Sie, dass Eigenschaften normalerweise aufzählbar sind (siehe Aufzählbarkeit: Best Practices), sodass Sie Object.keys() insbesondere für Objekte verwenden können, die Sie selbst erstellt haben.

Auflisten aller Eigenschaftsschlüssel

Wenn Sie alle Eigenschaften (sowohl eigene als auch geerbte) eines Objekts auflisten möchten, haben Sie zwei Möglichkeiten.

Option 1 ist die Verwendung der Schleife

for («variable» in «object»)
    «statement»

zum Iterieren über die Schlüssel aller aufzählbaren Eigenschaften von object. Siehe for-in für eine ausführlichere Beschreibung.

Option 2 ist die Implementierung einer eigenen Funktion, die über alle Eigenschaften (nicht nur aufzählbare) iteriert. Zum Beispiel

function getAllPropertyNames(obj) {
    var result = [];
    while (obj) {
        // Add the own property names of `obj` to `result`
        result = result.concat(Object.getOwnPropertyNames(obj));
        obj = Object.getPrototypeOf(obj);
    }
    return result;
}

Prüfen, ob eine Eigenschaft existiert

Sie können prüfen, ob ein Objekt eine Eigenschaft hat, oder ob eine Eigenschaft direkt innerhalb eines Objekts existiert:

Warnung

Vermeiden Sie den direkten Aufruf von hasOwnProperty() auf einem Objekt, da er überschrieben werden kann (z. B. durch eine eigene Eigenschaft mit dem Schlüssel hasOwnProperty)

> var obj = { hasOwnProperty: 1, foo: 2 };
> obj.hasOwnProperty('foo')  // unsafe
TypeError: Property 'hasOwnProperty' is not a function

Es ist besser, ihn generisch aufzurufen (siehe Generische Methoden: Methoden von Prototypen ausleihen)

> Object.prototype.hasOwnProperty.call(obj, 'foo')  // safe
true
> {}.hasOwnProperty.call(obj, 'foo')  // shorter
true

Beispiele

Die folgenden Beispiele basieren auf diesen Definitionen:

var proto = Object.defineProperties({}, {
    protoEnumTrue: { value: 1, enumerable: true },
    protoEnumFalse: { value: 2, enumerable: false }
});
var obj = Object.create(proto, {
    objEnumTrue: { value: 1, enumerable: true },
    objEnumFalse: { value: 2, enumerable: false }
});

Object.defineProperties() wird in Properties über Deskriptoren abrufen und definieren erklärt, aber es sollte ziemlich offensichtlich sein, wie es funktioniert: proto hat die eigenen Eigenschaften protoEnumTrue und protoEnumFalse und obj hat die eigenen Eigenschaften objEnumTrue und objEnumFalse (und erbt alle Eigenschaften von proto).

Hinweis

Beachten Sie, dass Objekte (wie proto im vorherigen Beispiel) normalerweise mindestens den Prototyp Object.prototype haben (wo Standardmethoden wie toString() und hasOwnProperty() definiert sind)

> Object.getPrototypeOf({}) === Object.prototype
true

Die Auswirkungen der Enumerabilität

Unter den property-bezogenen Operationen beeinflusst die Enumerabilität nur die for-in-Schleife und Object.keys() (sie beeinflusst auch JSON.stringify(), siehe JSON.stringify(value, replacer?, space?)).

Die for-in-Schleife iteriert über die Schlüssel aller aufzählbaren Eigenschaften, einschließlich der vererbten (beachten Sie, dass keine der nicht aufzählbaren Eigenschaften von Object.prototype angezeigt wird)

> for (var x in obj) console.log(x);
objEnumTrue
protoEnumTrue

Object.keys() gibt die Schlüssel aller eigenen (nicht vererbten) aufzählbaren Eigenschaften zurück

> Object.keys(obj)
[ 'objEnumTrue' ]

Wenn Sie die Schlüssel aller eigenen Eigenschaften wünschen, müssen Sie Object.getOwnPropertyNames() verwenden

> Object.getOwnPropertyNames(obj)
[ 'objEnumTrue', 'objEnumFalse' ]

Best Practices: Iterieren über eigene Eigenschaften

Um über Property-Schlüssel zu iterieren:

  • Kombinieren Sie for-in mit hasOwnProperty(), wie in for-in beschrieben. Dies funktioniert auch auf älteren JavaScript-Engines. Zum Beispiel

    for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            console.log(key);
        }
    }
  • Kombinieren Sie Object.keys() oder Object.getOwnPropertyNames() mit der Array-Iteration forEach()

    var obj = { first: 'John', last: 'Doe' };
    // Visit non-inherited enumerable keys
    Object.keys(obj).forEach(function (key) {
        console.log(key);
    });

Um über Property-Werte oder über (Schlüssel, Wert)-Paare zu iterieren

  • Iterieren Sie über die Schlüssel und verwenden Sie jeden Schlüssel, um den entsprechenden Wert abzurufen. Andere Sprachen vereinfachen dies, aber nicht JavaScript.

Accessors (Getter und Setter)

ECMAScript 5 ermöglicht es Ihnen, Methoden zu schreiben, deren Aufrufe so aussehen, als würden Sie eine Eigenschaft abrufen oder setzen. Das bedeutet, dass eine Eigenschaft virtuell ist und kein Speicherplatz. Sie könnten zum Beispiel das Setzen einer Eigenschaft verbieten und immer den Wert berechnen, der beim Lesen zurückgegeben wird.

Accessors über ein Objekt-Literal definieren

Das folgende Beispiel verwendet ein Objekt-Literal, um einen Setter und einen Getter für die Eigenschaft foo zu definieren:

var obj = {
    get foo() {
        return 'getter';
    },
    set foo(value) {
        console.log('setter: '+value);
    }
};

Hier ist die Interaktion

> obj.foo = 'bla';
setter: bla
> obj.foo
'getter'

Accessors über Property-Deskriptoren definieren

Eine alternative Möglichkeit, Getter und Setter zu spezifizieren, sind Property-Deskriptoren (siehe Property Deskriptoren). Der folgende Code definiert dasselbe Objekt wie das vorherige Literal

var obj = Object.create(
    Object.prototype, {  // object with property descriptors
        foo: {  // property descriptor
            get: function () {
                return 'getter';
            },
            set: function (value) {
                console.log('setter: '+value);
            }
        }
    }
);

In diesem Abschnitt betrachten wir die interne Struktur von Properties:

  • Property-Attribute sind die atomaren Bausteine von Properties.
  • Ein Property-Deskriptor ist eine Datenstruktur für die programmatische Arbeit mit Attributen.

Property-Attribute

Der gesamte Zustand eines Properties, sowohl seine Daten als auch seine Metadaten, wird in Attributen gespeichert. Dies sind Felder, die ein Property hat, ähnlich wie ein Objekt Properties hat. Attributschlüssel werden oft in doppelten Klammern geschrieben. Attribute sind für normale Properties und für Accessors (Getter und Setter) wichtig.

Die folgenden Attribute sind spezifisch für normale Properties

  • [[Value]] enthält den Wert des Properties, seine Daten.
  • [[Writable]] enthält einen booleschen Wert, der angibt, ob der Wert eines Properties geändert werden kann.

Die folgenden Attribute sind spezifisch für Accessors:

Alle Properties haben die folgenden Attribute:

Standardwerte

Wenn Sie keine Attribute angeben, werden die folgenden Standardwerte verwendet:

AttributschlüsselStandardwert

[[Value]]

undefined

[[Get]]

undefined

[[Set]]

undefined

[[Writable]]

false

[[Enumerable]]

false

[[Configurable]]

false

Diese Standardwerte sind wichtig, wenn Sie Properties über Property-Deskriptoren erstellen (siehe nächster Abschnitt).

Property-Deskriptoren werden für zwei Arten von Operationen verwendet:

Die folgenden Operationen ermöglichen es Ihnen, die Attribute eines Properties über Property-Deskriptoren abzurufen und festzulegen:

Object.getOwnPropertyDescriptor(obj, propKey)

Gibt den Deskriptor des eigenen (nicht vererbten) Properties von obj zurück, dessen Schlüssel propKey ist. Wenn kein solches Property existiert, wird undefined zurückgegeben.

> Object.getOwnPropertyDescriptor(Object.prototype, 'toString')
{ value: [Function: toString],
  writable: true,
  enumerable: false,
  configurable: true }

> Object.getOwnPropertyDescriptor({}, 'toString')
undefined
Object.defineProperty(obj, propKey, propDesc)

Erstellt oder ändert ein Property von obj, dessen Schlüssel propKey ist und dessen Attribute über propDesc spezifiziert sind. Gibt das geänderte Objekt zurück. Zum Beispiel

var obj = Object.defineProperty({}, 'foo', {
    value: 123,
    enumerable: true
    // writable: false (default value)
    // configurable: false (default value)
});
Object.defineProperties(obj, propDescObj)

Die Batch-Version von Object.defineProperty(). Jede Eigenschaft von propDescObj enthält einen Property-Deskriptor. Die Schlüssel der Eigenschaften und ihre Werte teilen Object.defineProperties mit, welche Properties auf obj erstellt oder geändert werden sollen. Zum Beispiel:

var obj = Object.defineProperties({}, {
    foo: { value: 123, enumerable: true },
    bar: { value: 'abc', enumerable: true }
});
Object.create(proto, propDescObj?)

Erstellen Sie zuerst ein Objekt, dessen Prototyp proto ist. Fügen Sie dann, falls der optionale Parameter propDescObj angegeben wurde, diesem Properties hinzu – auf die gleiche Weise wie Object.defineProperties. Geben Sie schließlich das Ergebnis zurück. Das folgende Code-Snippet erzeugt zum Beispiel dasselbe Ergebnis wie das vorherige Snippet

var obj = Object.create(Object.prototype, {
    foo: { value: 123, enumerable: true },
    bar: { value: 'abc', enumerable: true }
});

Um eine identische Kopie eines Objekts zu erstellen, müssen Sie zwei Dinge richtig machen:

  1. Die Kopie muss denselben Prototyp (siehe Layer 2: Die Prototyp-Beziehung zwischen Objekten) wie das Original haben.
  2. Die Kopie muss dieselben Eigenschaften mit denselben Attributen wie das Original haben.

Die folgende Funktion führt eine solche Kopie durch

function copyObject(orig) {
    // 1. copy has same prototype as orig
    var copy = Object.create(Object.getPrototypeOf(orig));

    // 2. copy has all of orig’s properties
    copyOwnPropertiesFrom(copy, orig);

    return copy;
}

Die Eigenschaften werden mit dieser Funktion von orig nach copy kopiert

function copyOwnPropertiesFrom(target, source) {
    Object.getOwnPropertyNames(source)  // (1)
    .forEach(function(propKey) {  // (2)
        var desc = Object.getOwnPropertyDescriptor(source, propKey); // (3)
        Object.defineProperty(target, propKey, desc);  // (4)
    });
    return target;
};

Dies sind die beteiligten Schritte

  1. Holen Sie sich ein Array mit den Schlüsseln aller eigenen Eigenschaften von source.
  2. Iterieren Sie über diese Schlüssel.
  3. Rufen Sie einen Property-Deskriptor ab.
  4. Verwenden Sie diesen Property-Deskriptor, um ein eigenes Property in target zu erstellen.

Beachten Sie, dass diese Funktion der Funktion _.extend() in der Underscore.js-Bibliothek sehr ähnlich ist.

Properties: Definition versus Zuweisung

Die folgenden beiden Operationen sind sehr ähnlich:

Es gibt jedoch einige subtile Unterschiede

  • Das Definieren eines Properties bedeutet, ein neues eigenes Property zu erstellen oder die Attribute eines bestehenden eigenen Properties zu aktualisieren. In beiden Fällen wird die Prototypenkette vollständig ignoriert.
  • Das Zuweisen zu einem Property prop bedeutet, ein bestehendes Property zu ändern. Der Prozess ist wie folgt:

    • Wenn prop ein Setter ist (eigen oder geerbt), rufen Sie diesen Setter auf.
    • Andernfalls, wenn prop schreibgeschützt ist (eigen oder geerbt), lösen Sie eine Ausnahme aus (im strikten Modus) oder tun Sie nichts (im schludrigen Modus). Der nächste Abschnitt erklärt dieses (etwas unerwartete) Phänomen ausführlicher.
    • Andernfalls, wenn prop ein eigenes (und beschreibbares) Property ist, ändern Sie den Wert dieses Properties.
    • Andernfalls existiert entweder kein Property prop, oder es ist geerbt und beschreibbar. In beiden Fällen wird ein eigenes Property prop definiert, das beschreibbar, konfigurierbar und aufzählbar ist. Im letzteren Fall haben wir gerade ein geerbtes Property überschrieben (nichtdestruktiv geändert). Im ersteren Fall wurde ein fehlendes Property automatisch definiert. Diese Art der automatischen Definition ist problematisch, da Tippfehler bei Zuweisungen schwer zu erkennen sein können.

Objekte schützen

Es gibt drei Stufen des Schutzes eines Objekts, hier von der schwächsten zur stärksten aufgelistet:

Versiegeln über

Object.seal(obj)

verhindert Erweiterungen und macht alle Properties „unkonfigurierbar“. Letzteres bedeutet, dass die Attribute (siehe Property-Attribute und Property-Deskriptoren) von Properties nicht mehr geändert werden können. Zum Beispiel bleiben schreibgeschützte Properties für immer schreibgeschützt.

Das folgende Beispiel zeigt, dass das Versiegeln alle Properties unkonfigurierbar macht

> var obj = { foo: 'a' };

> Object.getOwnPropertyDescriptor(obj, 'foo')  // before sealing
{ value: 'a',
  writable: true,
  enumerable: true,
  configurable: true }

> Object.seal(obj)

> Object.getOwnPropertyDescriptor(obj, 'foo')  // after sealing
{ value: 'a',
  writable: true,
  enumerable: true,
  configurable: false }

Sie können das Property foo immer noch ändern

> obj.foo = 'b';
'b'
> obj.foo
'b'

aber Sie können seine Attribute nicht ändern

> Object.defineProperty(obj, 'foo', { enumerable: false });
TypeError: Cannot redefine property: foo

Sie überprüfen, ob ein Objekt versiegelt ist, über

Object.isSealed(obj)

Eine Konstruktorfunktion (kurz: Konstruktor) hilft bei der Erzeugung von Objekten, die in gewisser Weise ähnlich sind. Es ist eine normale Funktion, wird aber anders benannt, eingerichtet und aufgerufen. Dieser Abschnitt erklärt, wie Konstruktoren funktionieren. Sie entsprechen Klassen in anderen Sprachen.

Wir haben bereits ein Beispiel für zwei ähnliche Objekte gesehen (in Daten zwischen Objekten über einen Prototyp teilen)

var PersonProto = {
    describe: function () {
        return 'Person named '+this.name;
    }
};
var jane = {
    [[Prototype]]: PersonProto,
    name: 'Jane'
};
var tarzan = {
    [[Prototype]]: PersonProto,
    name: 'Tarzan'
};

Die Objekte jane und tarzan werden beide als „Personen“ betrachtet und teilen sich das Prototypobjekt PersonProto. Wandeln wir diesen Prototyp in einen Konstruktor Person um, der Objekte wie jane und tarzan erstellt. Die Objekte, die ein Konstruktor erstellt, werden als seine Instanzen bezeichnet. Solche Instanzen haben dieselbe Struktur wie jane und tarzan und bestehen aus zwei Teilen

  1. Daten sind instanzspezifisch und werden in den eigenen Eigenschaften der Instanzobjekte gespeichert (jane und tarzan im vorherigen Beispiel).
  2. Verhalten wird von allen Instanzen geteilt – sie haben ein gemeinsames Prototypobjekt mit Methoden (PersonProto im vorherigen Beispiel).

Ein Konstruktor ist eine Funktion, die über den new-Operator aufgerufen wird. Nach Konvention beginnen die Namen von Konstruktoren mit Großbuchstaben, während die Namen von normalen Funktionen und Methoden mit Kleinbuchstaben beginnen. Die Funktion selbst richtet Teil 1 ein

function Person(name) {
    this.name = name;
}

Das Objekt in Person.prototype wird zum Prototyp aller Instanzen von Person. Es trägt Teil 2 bei

Person.prototype.describe = function () {
    return 'Person named '+this.name;
};

Erstellen und verwenden wir eine Instanz von Person

> var jane = new Person('Jane');
> jane.describe()
'Person named Jane'

Wir sehen, dass Person eine normale Funktion ist. Sie wird nur dann zu einem Konstruktor, wenn sie über new aufgerufen wird. Der new-Operator führt folgende Schritte aus:

Abbildung 17-3 zeigt, wie die Instanz jane aussieht. Die Eigenschaft constructor von Person.prototype zeigt zurück auf den Konstruktor und wird in Die constructor-Eigenschaft von Instanzen erläutert.

Der Operator instanceof ermöglicht uns zu überprüfen, ob ein Objekt eine Instanz eines bestimmten Konstruktors ist

> jane instanceof Person
true
> jane instanceof Date
false

Standardmäßig enthält jede Funktion C ein Instanz-Prototyp-Objekt C.prototype, dessen Eigenschaft constructor zurück auf C zeigt:

> function C() {}
> C.prototype.constructor === C
true

Da die constructor-Eigenschaft von jedem Instanz-Prototyp von jeder Instanz geerbt wird, können Sie sie verwenden, um den Konstruktor einer Instanz zu erhalten

> var o = new C();
> o.constructor
[Function: C]

Anwendungsfälle für die constructor-Eigenschaft

Umschalten des Konstruktors eines Objekts

In der folgenden catch-Klausel treffen wir unterschiedliche Maßnahmen, abhängig vom Konstruktor der abgefangenen Ausnahme:

try {
    ...
} catch (e) {
    switch (e.constructor) {
        case SyntaxError:
            ...
            break;
        case CustomError:
            ...
            break;
        ...
    }
}

Warnung

Dieser Ansatz erkennt nur direkte Instanzen eines bestimmten Konstruktors. Im Gegensatz dazu erkennt instanceof sowohl direkte Instanzen als auch Instanzen aller Unterkonstruktoren.

Ermitteln des Namens des Konstruktors eines Objekts

Zum Beispiel

> function Foo() {}
> var f = new Foo();
> f.constructor.name
'Foo'

Warnung

Nicht alle JavaScript-Engines unterstützen die Eigenschaft name für Funktionen.

Erstellen ähnlicher Objekte

So erstellen Sie ein neues Objekt, y, das denselben Konstruktor wie ein vorhandenes Objekt, x, hat

function Constr() {}
var x = new Constr();

var y = new x.constructor();
console.log(y instanceof Constr); // true

Dieser Trick ist nützlich für eine Methode, die für Instanzen von Unterkonstruktoren funktionieren muss und eine neue Instanz erstellen möchte, die this ähnelt. Dann können Sie keinen festen Konstruktor verwenden

SuperConstr.prototype.createCopy = function () {
    return new this.constructor(...);
};
Verweis auf einen Superkonstruktor

Einige Vererbungsbibliotheken weisen dem Subkonstruktor eine Super-Prototyp-Eigenschaft zu. Zum Beispiel bietet das YUI-Framework Unterklassenbildung über Y.extend

function Super() {
}
function Sub() {
    Sub.superclass.constructor.call(this); // (1)
}
Y.extend(Sub, Super);

Der Aufruf in Zeile (1) funktioniert, da extend Sub.superclass auf Super.prototype setzt. Dank der constructor-Eigenschaft können Sie den Superkonstruktor als Methode aufrufen.

Hinweis

Der Operator instanceof (siehe Der instanceof-Operator) ist nicht auf die constructor-Eigenschaft angewiesen.

Der Operator instanceof

value instanceof Constr

bestimmt, ob value von dem Konstruktor Constr oder einem Unterkonstruktor erstellt wurde. Dies geschieht, indem geprüft wird, ob Constr.prototype in der Prototypenkette von value enthalten ist. Daher sind die folgenden beiden Ausdrücke äquivalent:

value instanceof Constr
Constr.prototype.isPrototypeOf(value)

Hier sind einige Beispiele

> {} instanceof Object
true

> [] instanceof Array  // constructor of []
true
> [] instanceof Object  // super-constructor of []
true

> new Date() instanceof Date
true
> new Date() instanceof Object
true

Wie erwartet, ist instanceof immer false für primitive Werte

> 'abc' instanceof Object
false
> 123 instanceof Number
false

Schließlich löst instanceof eine Ausnahme aus, wenn seine rechte Seite keine Funktion ist

> [] instanceof 123
TypeError: Expecting a function in instanceof check

Fast alle Objekte sind Instanzen von Object, da Object.prototype in ihrer Prototypenkette liegt. Es gibt aber auch Objekte, bei denen das nicht der Fall ist. Hier sind zwei Beispiele:

> Object.create(null) instanceof Object
false
> Object.prototype instanceof Object
false

Das erstere Objekt wird im Detail in Das dict-Muster: Objekte ohne Prototypen sind bessere Maps erläutert. Das letztere Objekt ist dort, wo die meisten Prototypketten enden (und sie müssen irgendwo enden). Beide Objekte haben keinen Prototyp

> Object.getPrototypeOf(Object.create(null))
null
> Object.getPrototypeOf(Object.prototype)
null

Aber typeof klassifiziert sie korrekt als Objekte

> typeof Object.create(null)
'object'
> typeof Object.prototype
'object'

Dieser Fallstrick ist für die meisten Anwendungsfälle von instanceof kein Dealbreaker, aber man muss sich dessen bewusst sein.

Fallstrick: Realms (Frames oder Fenster) überqueren

In Webbrowsern hat jeder Frame und jedes Fenster seinen eigenen Realm mit separaten globalen Variablen. Das verhindert, dass instanceof für Objekte funktioniert, die Realms überqueren. Um zu sehen, warum, betrachten Sie den folgenden Code:

if (myvar instanceof Array) ...  // Doesn’t always work

Wenn myvar ein Array aus einem anderen Realm ist, dann ist sein Prototyp das Array.prototype aus diesem Realm. Daher findet instanceof das Array.prototype des aktuellen Realms nicht in der Prototypenkette von myvar und gibt false zurück. ECMAScript 5 hat die Funktion Array.isArray(), die immer funktioniert:

<head>
    <script>
        function test(arr) {
            var iframe = frames[0];

            console.log(arr instanceof Array); // false
            console.log(arr instanceof iframe.Array); // true
            console.log(Array.isArray(arr)); // true
        }
    </script>
</head>
<body>
    <iframe srcdoc="<script>window.parent.test([])</script>">
    </iframe>
</body>

Offensichtlich ist dies auch ein Problem mit Nicht-Built-in-Konstruktoren.

Abgesehen von der Verwendung von Array.isArray() gibt es mehrere Dinge, die Sie tun können, um dieses Problem zu umgehen

  • Vermeiden Sie Objekte, die Realms überqueren. Browser haben die postMessage()-Methode, die ein Objekt in einen anderen Realm kopieren kann, anstatt eine Referenz zu übergeben.
  • Überprüfen Sie den Namen des Konstruktors einer Instanz (funktioniert nur auf Engines, die die Eigenschaft name für Funktionen unterstützen)

    someValue.constructor.name === 'NameOfExpectedConstructor'
  • Verwenden Sie eine Prototyp-Eigenschaft, um Instanzen als zu einem Typ T gehörig zu markieren. Es gibt mehrere Möglichkeiten, dies zu tun. Die Prüfungen, ob value eine Instanz von T ist, sehen wie folgt aus

    • value.isT(): Der Prototyp von T-Instanzen muss von dieser Methode true zurückgeben; ein gemeinsamer Superkonstruktor sollte den Standardwert false zurückgeben.
    • 'T' in value: Sie müssen den Prototyp von T-Instanzen mit einer Eigenschaft versehen, deren Schlüssel 'T' (oder etwas Eindeutigeres) ist.
    • value.TYPE_NAME === 'T': Jeder relevante Prototyp muss eine TYPE_NAME-Eigenschaft mit einem geeigneten Wert haben.

Dieser Abschnitt gibt einige Tipps zur Implementierung von Konstruktoren.

Schutz vor dem Vergessen von new: Strict Mode

Wenn Sie new vergessen, wenn Sie einen Konstruktor verwenden, rufen Sie ihn als Funktion und nicht als Konstruktor auf. Im "sloppy mode" erhalten Sie keine Instanz und globale Variablen werden erstellt. Leider geschieht all dies ohne Vorwarnung:

function SloppyColor(name) {
    this.name = name;
}
var c = SloppyColor('green'); // no warning!

// No instance is created:
console.log(c); // undefined
// A global variable is created:
console.log(name); // green

Im strikten Modus erhalten Sie eine Ausnahme

function StrictColor(name) {
    'use strict';
    this.name = name;
}
var c = StrictColor('green');
// TypeError: Cannot set property 'name' of undefined

Beliebige Objekte von einem Konstruktor zurückgeben

In vielen objektorientierten Sprachen können Konstruktoren nur direkte Instanzen erzeugen. Betrachten Sie zum Beispiel Java: Nehmen wir an, Sie möchten eine Klasse Expression implementieren, die die Unterklassen Addition und Multiplication hat. Das Parsen erzeugt direkte Instanzen der beiden letzteren Klassen. Sie können dies nicht als Konstruktor von Expression implementieren, da dieser Konstruktor nur direkte Instanzen von Expression erzeugen kann. Als Workaround werden in Java statische Factory-Methoden verwendet:

class Expression {
    // Static factory method:
    public static Expression parse(String str) {
        if (...) {
            return new Addition(...);
        } else if (...) {
            return new Multiplication(...);
        } else {
            throw new ExpressionException(...);
        }
    }
}
...
Expression expr = Expression.parse(someStr);

In JavaScript können Sie einfach jedes benötigte Objekt von einem Konstruktor zurückgeben. Die JavaScript-Version des vorherigen Codes würde also wie folgt aussehen:

function Expression(str) {
    if (...) {
        return new Addition(..);
    } else if (...) {
        return new Multiplication(...);
    } else {
        throw new ExpressionException(...);
    }
}
...
var expr = new Expression(someStr);

Das sind gute Nachrichten: JavaScript-Konstruktoren sind nicht starr, sodass Sie Ihre Meinung jederzeit ändern können, ob ein Konstruktor eine direkte Instanz oder etwas anderes zurückgeben soll.

Daten in Prototyp-Eigenschaften

Dieser Abschnitt erklärt, dass Sie in den meisten Fällen keine Daten in Prototyp-Eigenschaften speichern sollten. Es gibt jedoch einige Ausnahmen von dieser Regel.

Vermeiden Sie Prototyp-Eigenschaften mit Anfangswerten für Instanz-Eigenschaften

Prototypen enthalten Eigenschaften, die von mehreren Objekten gemeinsam genutzt werden. Daher eignen sie sich gut für Methoden. Zusätzlich können Sie mit einer als nächstes beschriebenen Technik auch Anfangswerte für Instanz-Eigenschaften bereitstellen. Ich werde später erklären, warum dies nicht empfohlen wird.

Ein Konstruktor setzt normalerweise Instanz-Eigenschaften auf Anfangswerte. Wenn einer dieser Werte ein Standardwert ist, müssen Sie keine Instanz-Eigenschaft erstellen. Sie benötigen nur eine Prototyp-Eigenschaft mit demselben Schlüssel, deren Wert der Standardwert ist. Zum Beispiel:

/**
 * Anti-pattern: don’t do this
 *
 * @param data an array with names
 */
function Names(data) {
    if (data) {
        // There is a parameter
        // => create instance property
        this.data = data;
    }
}
Names.prototype.data = [];

Der Parameter data ist optional. Wenn er fehlt, erhält die Instanz keine data-Eigenschaft, sondern erbt stattdessen Names.prototype.data.

Dieser Ansatz funktioniert größtenteils: Sie können eine Instanz n von Names erstellen. Das Abrufen von n.data liest Names.prototype.data. Das Setzen von n.data erstellt eine neue eigene Eigenschaft in n und behält den gemeinsam genutzten Standardwert im Prototyp bei. Wir haben nur ein Problem, wenn wir den Standardwert ändern (anstatt ihn durch einen neuen Wert zu ersetzen).

> var n1 = new Names();
> var n2 = new Names();

> n1.data.push('jane'); // changes default value
> n1.data
[ 'jane' ]

> n2.data
[ 'jane' ]

Im vorherigen Beispiel hat push() das Array in Names.prototype.data geändert. Da dieses Array von allen Instanzen ohne eigene data-Eigenschaft gemeinsam genutzt wird, hat sich auch der Anfangswert von n2.data geändert.

Best Practice: Standardwerte nicht gemeinsam nutzen

Angesichts dessen, was wir gerade besprochen haben, ist es besser, keine Standardwerte gemeinsam zu nutzen und immer neue zu erstellen.

function Names(data) {
    this.data = data || [];
}

Offensichtlich tritt das Problem der Änderung eines gemeinsam genutzten Standardwerts nicht auf, wenn dieser Wert unveränderlich ist (wie alle Primitiven; siehe Primitive Values). Aber der Konsistenz halber ist es am besten, sich an eine einzige Methode zur Einrichtung von Eigenschaften zu halten. Ich bevorzuge es auch, die übliche Trennung der Zuständigkeiten beizubehalten (siehe Layer 3: Constructors—Factories for Instances): Der Konstruktor richtet die Instanzeigenschaften ein und der Prototyp enthält die Methoden.

ECMAScript 6 wird dies zu einer noch besseren Praxis machen, da Konstruktorparameter Standardwerte haben können und Sie Prototyp-Methoden über Klassen definieren können, aber keine Prototyp-Eigenschaften mit Daten.

Instanzeigenschaften bei Bedarf erstellen

Gelegentlich ist die Erstellung eines Eigenschaftswerts ein teurer Vorgang (rechnerisch oder speicherintensiv). In diesem Fall können Sie eine Instanzeigenschaft bei Bedarf erstellen:

function Names(data) {
    if (data) this.data = data;
}
Names.prototype = {
    constructor: Names, // (1)
    get data() {
        // Define, don’t assign
        // => avoid calling the (nonexistent) setter
        Object.defineProperty(this, 'data', {
            value: [],
            enumerable: true,
            configurable: false,
            writable: false
        });
        return this.data;
    }
};

Wir können die Eigenschaft data nicht über eine Zuweisung zur Instanz hinzufügen, da JavaScript über einen fehlenden Setter klagen würde (was es tut, wenn es nur einen Getter findet). Daher fügen wir sie über Object.defineProperty() hinzu. Konsultieren Sie Properties: Definition Versus Assignment, um die Unterschiede zwischen Definition und Zuweisung zu wiederholen. In Zeile (1) stellen wir sicher, dass die constructor-Eigenschaft korrekt eingerichtet ist (siehe The constructor Property of Instances).

Offensichtlich ist das viel Arbeit, daher müssen Sie sicher sein, dass es sich lohnt.

Vermeiden Sie nicht-polymorphe Prototyp-Eigenschaften

Wenn dieselbe Eigenschaft (gleicher Schlüssel, gleiche Semantik, im Allgemeinen unterschiedliche Werte) in mehreren Prototypen vorhanden ist, wird sie als polymorph bezeichnet. Dann wird das Ergebnis des Lesens der Eigenschaft über eine Instanz dynamisch über den Prototyp dieser Instanz bestimmt. Nicht-polymorph verwendete Prototyp-Eigenschaften können durch Variablen ersetzt werden (was ihre nicht-polymorphe Natur besser widerspiegelt).

Sie können beispielsweise eine Konstante in einer Prototyp-Eigenschaft speichern und über this darauf zugreifen.

function Foo() {}
Foo.prototype.FACTOR = 42;
Foo.prototype.compute = function (x) {
    return x * this.FACTOR;
};

Diese Konstante ist nicht polymorph. Daher können Sie genauso gut über eine Variable darauf zugreifen.

// This code should be inside an IIFE or a module
function Foo() {}
var FACTOR = 42;
Foo.prototype.compute = function (x) {
    return x * FACTOR;
};

Polymorphe Prototyp-Eigenschaften

Hier ist ein Beispiel für polymorphe Prototyp-Eigenschaften mit unveränderlichen Daten. Durch das Tagging von Instanzen eines Konstruktors über Prototyp-Eigenschaften können Sie diese von Instanzen eines anderen Konstruktors unterscheiden:

function ConstrA() { }
ConstrA.prototype.TYPE_NAME = 'ConstrA';

function ConstrB() { }
ConstrB.prototype.TYPE_NAME = 'ConstrB';

Dank des polymorphen "Tags" TYPE_NAME können Sie die Instanzen von ConstrA und ConstrB unterscheiden, selbst wenn sie Realms überschreiten (dann funktioniert instanceof nicht; siehe Pitfall: crossing realms (frames or windows)).

Daten privat halten

JavaScript hat keine dedizierten Mittel zur Verwaltung privater Daten für ein Objekt. Dieser Abschnitt beschreibt drei Techniken, um diese Einschränkung zu umgehen:

  • Private Daten in der Umgebung eines Konstruktors
  • Private Daten in Eigenschaften mit gekennzeichneten Schlüsseln
  • Private Daten in Eigenschaften mit reifiziertem Schlüssel

Zusätzlich werde ich erklären, wie globale Daten über IIFEs privat gehalten werden.

Private Daten in der Umgebung eines Konstruktors (Crockford Privacy Pattern)

Wenn ein Konstruktor aufgerufen wird, werden zwei Dinge erstellt: die Instanz des Konstruktors und eine Umgebung (siehe Environments: Managing Variables). Die Instanz soll vom Konstruktor initialisiert werden. Die Umgebung enthält die Parameter und lokalen Variablen des Konstruktors. Jede Funktion (einschließlich Methoden), die innerhalb des Konstruktors erstellt wird, behält eine Referenz auf die Umgebung – die Umgebung, in der sie erstellt wurde. Dank dieser Referenz hat sie immer Zugriff auf die Umgebung, auch nachdem der Konstruktor beendet ist. Diese Kombination aus Funktion und Umgebung wird als Closure bezeichnet (siehe Closures: Functions Stay Connected to Their Birth Scopes). Die Umgebung des Konstruktors ist somit ein Datenspeicher, der von der Instanz unabhängig ist und nur mit ihr in Beziehung steht, weil beide zur gleichen Zeit erstellt werden. Um sie richtig zu verbinden, müssen wir Funktionen haben, die in beiden Welten leben. Nach Douglas Crockfords Terminologie kann eine Instanz drei Arten von Werten zugeordnet sein (siehe Figure 17-4):

Öffentliche Eigenschaften
Werte, die in Eigenschaften gespeichert sind (entweder in der Instanz oder in ihrem Prototyp), sind öffentlich zugänglich.
Private Werte
Daten und Funktionen, die in der Umgebung gespeichert sind, sind privat – nur für den Konstruktor und die von ihm erstellten Funktionen zugänglich.
Privilegierte Methoden
Private Funktionen können auf öffentliche Eigenschaften zugreifen, aber öffentliche Methoden im Prototyp können nicht auf private Daten zugreifen. Wir benötigen also privilegierte Methoden – öffentliche Methoden in der Instanz. Privilegierte Methoden sind öffentlich und können von jedem aufgerufen werden, aber sie haben auch Zugriff auf private Werte, da sie im Konstruktor erstellt wurden.

Die folgenden Abschnitte erklären jede Art von Wert genauer.

Öffentliche Eigenschaften

Denken Sie daran, dass es für einen Konstruktor Constr zwei Arten von Eigenschaften gibt, die öffentlich sind und für jeden zugänglich sind. Erstens sind Prototyp-Eigenschaften in Constr.prototype gespeichert und werden von allen Instanzen gemeinsam genutzt. Prototyp-Eigenschaften sind normalerweise Methoden:

Constr.prototype.publicMethod = ...;

Zweitens sind Instanz-Eigenschaften für jede Instanz einzigartig. Sie werden im Konstruktor hinzugefügt und enthalten normalerweise Daten (keine Methoden):

function Constr(...) {
    this.publicData = ...;
    ...
}

Private Werte

Die Umgebung des Konstruktors besteht aus den Parametern und lokalen Variablen. Sie sind nur von innerhalb des Konstruktors zugänglich und somit privat für die Instanz:

function Constr(...) {
    ...
    var that = this; // make accessible to private functions

    var privateData = ...;

    function privateFunction(...) {
        // Access everything
        privateData = ...;

        that.publicData = ...;
        that.publicMethod(...);
    }
    ...
}

Privilegierte Methoden

Private Daten sind so sicher vor externem Zugriff, dass Prototyp-Methoden nicht darauf zugreifen können. Aber wie sollte man sie sonst nach Verlassen des Konstruktors verwenden? Die Antwort sind privilegierte Methoden: Im Konstruktor erstellte Funktionen werden als Instanz-Methoden hinzugefügt. Das bedeutet, dass sie einerseits auf private Daten zugreifen können; andererseits sind sie öffentlich und werden daher von Prototyp-Methoden gesehen. Mit anderen Worten, sie dienen als Vermittler zwischen privaten Daten und der Öffentlichkeit (einschließlich Prototyp-Methoden):

function Constr(...) {
    ...
    this.privilegedMethod = function (...) {
        // Access everything
        privateData = ...;
        privateFunction(...);

        this.publicData = ...;
        this.publicMethod(...);
    };
}

Ein Beispiel

Das Folgende ist eine Implementierung eines StringBuilder unter Verwendung des Crockford Privacy Patterns:

function StringBuilder() {
    var buffer = [];
    this.add = function (str) {
        buffer.push(str);
    };
    this.toString = function () {
        return buffer.join('');
    };
}
// Can’t put methods in the prototype!

Hier ist die Interaktion

> var sb = new StringBuilder();
> sb.add('Hello');
> sb.add(' world!');
> sb.toString()
’Hello world!’

Vor- und Nachteile des Crockford Privacy Patterns

Hier sind einige Punkte, die Sie bei der Verwendung des Crockford Privacy Patterns berücksichtigen sollten:

Es ist nicht sehr elegant
Der Vermittlungszugriff auf private Daten über privilegierte Methoden führt zu einer unnötigen Umleitung. Privilegierte Methoden und private Funktionen zerstören beide die Trennung der Zuständigkeiten zwischen dem Konstruktor (Einrichtung von Instanzdaten) und dem Instanzprototyp (Methoden).
Es ist völlig sicher
Es gibt keine Möglichkeit, von außen auf die Daten der Umgebung zuzugreifen, was diese Lösung sicher macht, wenn Sie sie benötigen (z. B. für sicherheitskritischen Code). Andererseits kann der Ausschluss privater Daten von außen auch ein Nachteil sein. Manchmal möchten Sie private Funktionalitäten testen. Und einige vorübergehende schnelle Korrekturen hängen von der Möglichkeit ab, auf private Daten zuzugreifen. Diese Art von schnellen Korrekturen kann nicht vorhergesagt werden, daher kann der Bedarf entstehen, egal wie gut Ihr Design ist.
Es kann langsamer sein
Der Zugriff auf Eigenschaften in der Prototypenkette ist in aktuellen JavaScript-Engines hoch optimiert. Der Zugriff auf Werte im Closure kann langsamer sein. Aber diese Dinge ändern sich ständig, daher müssen Sie messen, ob dies für Ihren Code wirklich wichtig ist.
Es verbraucht mehr Speicher
Das Beibehalten der Umgebung und das Platzieren privilegierter Methoden in Instanzen kostet Speicher. Auch hier gilt: Stellen Sie sicher, dass es für Ihren Code wirklich wichtig ist, und messen Sie.

Private Daten in Eigenschaften mit gekennzeichneten Schlüsseln

Für die meisten nicht sicherheitskritischen Anwendungen ist Privatsphäre eher ein Hinweis für API-Kunden: „Das müssen Sie nicht sehen.“ Das ist der Hauptvorteil der Kapselung – Verbergen von Komplexität. Obwohl intern mehr passiert, müssen Sie nur den öffentlichen Teil einer API verstehen. Die Idee einer Namenskonvention besteht darin, Kunden über die Privatsphäre zu informieren, indem der Schlüssel einer Eigenschaft gekennzeichnet wird. Ein vorangestellter Unterstrich wird dafür oft verwendet.

Schreiben wir das vorherige StringBuilder-Beispiel neu, sodass der Puffer in einer Eigenschaft _buffer gespeichert wird, die aber nur konventionsgemäß privat ist.

function StringBuilder() {
    this._buffer = [];
}
StringBuilder.prototype = {
    constructor: StringBuilder,
    add: function (str) {
        this._buffer.push(str);
    },
    toString: function () {
        return this._buffer.join('');
    }
};

Hier sind einige Vor- und Nachteile der Privatsphäre über gekennzeichnete Eigenschaftsschlüssel:

Es bietet einen natürlicheren Programmierstil
Der Zugriff auf private und öffentliche Daten auf dieselbe Weise ist eleganter als die Verwendung von Umgebungen für die Privatsphäre.
Es verschmutzt den Namensraum der Eigenschaften
Eigenschaften mit gekennzeichneten Schlüsseln sind überall sichtbar. Je mehr Leute IDEs verwenden, desto lästiger wird es, dass sie neben öffentlichen Eigenschaften angezeigt werden, an Stellen, an denen sie verborgen sein sollten. IDEs könnten theoretisch Anpassungen vornehmen, indem sie Namenskonventionen erkennen und private Eigenschaften nach Möglichkeit ausblenden.
Private Eigenschaften können von "außen" zugegriffen werden
Das kann für Unit-Tests und schnelle Korrekturen nützlich sein. Zusätzlich können Unterkonstruktoren und Hilfsfunktionen (sogenannte "friend functions") von einem einfacheren Zugriff auf private Daten profitieren. Der Umgebungsansatz bietet diese Flexibilität nicht; private Daten können nur aus dem Konstruktor heraus zugegriffen werden.
Es kann zu Schlüsselkonflikten führen
Schlüssel privater Eigenschaften können kollidieren. Dies ist bereits ein Problem für Unterkonstruktoren, aber noch problematischer, wenn Sie mit Mehrfachvererbung arbeiten (wie sie von einigen Bibliotheken ermöglicht wird). Mit dem Umgebungsansatz gibt es nie Kollisionen.

Private Daten in Eigenschaften mit reifiziertem Schlüssel

Ein Problem bei einer Namenskonvention für private Eigenschaften ist, dass Schlüssel kollidieren können (z. B. ein Schlüssel von einem Konstruktor mit einem Schlüssel von einem Unterkonstruktor oder ein Schlüssel von einem Mixin mit einem Schlüssel von einem Konstruktor). Sie können solche Kollisionen unwahrscheinlicher machen, indem Sie längere Schlüssel verwenden, die zum Beispiel den Namen des Konstruktors enthalten. Dann würde im vorherigen Fall die private Eigenschaft _buffer _StringBuilder_buffer heißen. Wenn ein solcher Schlüssel zu lang für Ihren Geschmack ist, haben Sie die Möglichkeit, ihn zu reifizieren, indem Sie ihn in einer Variablen speichern:

var KEY_BUFFER = '_StringBuilder_buffer';

Wir greifen nun über this[KEY_BUFFER] auf die privaten Daten zu.

var StringBuilder = function () {
    var KEY_BUFFER = '_StringBuilder_buffer';

    function StringBuilder() {
        this[KEY_BUFFER] = [];
    }
    StringBuilder.prototype = {
        constructor: StringBuilder,
        add: function (str) {
            this[KEY_BUFFER].push(str);
        },
        toString: function () {
            return this[KEY_BUFFER].join('');
        }
    };
    return StringBuilder;
}();

Wir haben eine IIFE um StringBuilder gewickelt, damit die Konstante KEY_BUFFER lokal bleibt und den globalen Namensraum nicht verschmutzt.

Reifizierte Eigenschaftsschlüssel ermöglichen die Verwendung von UUIDs (universally unique identifiers) in Schlüsseln. Zum Beispiel über Robert Kieffers node-uuid.

var KEY_BUFFER = '_StringBuilder_buffer_' + uuid.v4();

KEY_BUFFER hat jedes Mal, wenn der Code ausgeführt wird, einen anderen Wert. Er kann zum Beispiel so aussehen:

_StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1

Lange Schlüssel mit UUIDs machen Schlüsselkonflikte praktisch unmöglich.

Globale Daten über IIFEs privat halten

Diese Unterabschnitt erklärt, wie globale Daten für Singleton-Objekte, Konstruktoren und Methoden privat gehalten werden können, über IIFEs (siehe Introducing a New Scope via an IIFE). Diese IIFEs erstellen neue Umgebungen (siehe Environments: Managing Variables), in denen Sie die privaten Daten platzieren.

Private globale Daten an ein Singleton-Objekt anhängen

Sie benötigen keinen Konstruktor, um ein Objekt mit privaten Daten in einer Umgebung zu verknüpfen. Das folgende Beispiel zeigt, wie Sie eine IIFE für denselben Zweck verwenden, indem Sie sie um ein Singleton-Objekt wickeln:

var obj = function () {  // open IIFE

    // public
    var self = {
        publicMethod: function (...) {
            privateData = ...;
            privateFunction(...);
        },
        publicData: ...
    };

    // private
    var privateData = ...;
    function privateFunction(...) {
        privateData = ...;
        self.publicData = ...;
        self.publicMethod(...);
    }

    return self;
}(); // close IIFE

Globale Daten für einen gesamten Konstruktor privat halten

Einige globale Daten sind nur für einen Konstruktor und die Prototyp-Methoden relevant. Indem Sie eine IIFE um beides wickeln, können Sie sie vor der Öffentlichkeit verbergen. Private Data in Properties with Reified Keys gab ein Beispiel: Der Konstruktor StringBuilder und seine Prototyp-Methoden verwenden die Konstante KEY_BUFFER, die einen Eigenschaftsschlüssel enthält. Diese Konstante ist in der Umgebung einer IIFE gespeichert.

var StringBuilder = function () { // open IIFE
    var KEY_BUFFER = '_StringBuilder_buffer_' + uuid.v4();

    function StringBuilder() {
        this[KEY_BUFFER] = [];
    }
    StringBuilder.prototype = {
        // Omitted: methods accessing this[KEY_BUFFER]
    };
    return StringBuilder;
}(); // close IIFE

Beachten Sie, dass Sie, wenn Sie ein Modulsystem verwenden (siehe Chapter 31), denselben Effekt mit saubererem Code erzielen können, indem Sie den Konstruktor plus Methoden in ein Modul legen.

Globale Daten an eine Methode anhängen

Manchmal benötigen Sie globale Daten nur für eine einzige Methode. Sie können sie privat halten, indem Sie sie in der Umgebung einer IIFE platzieren, die Sie um die Methode wickeln. Zum Beispiel:

var obj = {
    method: function () {  // open IIFE

        // method-private data
        var invocCount = 0;

        return function () {
            invocCount++;
            console.log('Invocation #'+invocCount);
            return 'result';
        };
    }()  // close IIFE
};

Hier ist die Interaktion

> obj.method()
Invocation #1
'result'
> obj.method()
Invocation #2
'result'

Schicht 4: Vererbung zwischen Konstruktoren

In diesem Abschnitt untersuchen wir, wie Konstruktoren vererbt werden können: Gegeben einen Konstruktor Super, wie können wir einen neuen Konstruktor, Sub, schreiben, der alle Merkmale von Super plus einige eigene Merkmale hat? Leider hat JavaScript keinen eingebauten Mechanismus, um diese Aufgabe auszuführen. Daher müssen wir einige manuelle Arbeiten leisten.

Abbildung 17-5 veranschaulicht die Idee: Der Subkonstruktor Sub sollte alle Eigenschaften von Super (sowohl Prototyp-Eigenschaften als auch Instanz-Eigenschaften) zusätzlich zu seinen eigenen haben. Somit haben wir eine grobe Vorstellung davon, wie Sub aussehen sollte, wissen aber nicht, wie wir dorthin gelangen. Es gibt mehrere Dinge, die wir herausfinden müssen, die ich als nächstes erklären werde.

  • Instanzeigenschaften vererben.
  • Prototyp-Eigenschaften vererben.
  • Sicherstellen, dass instanceof funktioniert: Wenn sub eine Instanz von Sub ist, wollen wir auch, dass sub instanceof Super wahr ist.
  • Eine Methode überschreiben, um eine der Super-Methoden in Sub anzupassen.
  • Super-Aufrufe tätigen: Wenn wir eine der Super-Methoden überschrieben haben, müssen wir möglicherweise die ursprüngliche Methode aus Sub aufrufen.

Instanzeigenschaften vererben

Instanzeigenschaften werden im Konstruktor selbst eingerichtet, daher beinhaltet das Vererben der Instanzeigenschaften des Superkonstruktors das Aufrufen dieses Konstruktors:

function Sub(prop1, prop2, prop3, prop4) {
    Super.call(this, prop1, prop2);  // (1)
    this.prop3 = prop3;  // (2)
    this.prop4 = prop4;  // (3)
}

Wenn Sub über new aufgerufen wird, bezieht sich sein impliziter Parameter this auf eine frische Instanz. Sie übergibt diese Instanz zuerst an Super (1), das seine Instanzeigenschaften hinzufügt. Danach richtet Sub seine eigenen Instanzeigenschaften ein (2,3). Der Trick ist, Super nicht über new aufzurufen, da dies eine frische Superinstanz erzeugen würde. Stattdessen rufen wir Super als Funktion auf und übergeben die aktuelle (Sub-)Instanz als Wert von this.

Prototyp-Eigenschaften vererben

Gemeinsam genutzte Eigenschaften wie Methoden werden im Instanzprototyp gespeichert. Daher müssen wir einen Weg finden, damit Sub.prototype alle Eigenschaften von Super.prototype erbt. Die Lösung besteht darin, Sub.prototype den Prototyp Super.prototype zu geben.

Verwirrt von den beiden Arten von Prototypen?

Ja, die JavaScript-Terminologie ist hier verwirrend. Wenn Sie sich verloren fühlen, konsultieren Sie Terminology: The Two Prototypes, das erklärt, wie sie sich unterscheiden.

Dies ist der Code, der dies erreicht.

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.methodB = ...;
Sub.prototype.methodC = ...;

Object.create() erzeugt ein frisches Objekt, dessen Prototyp Super.prototype ist. Danach fügen wir Subs Methoden hinzu. Wie in The constructor Property of Instances erklärt, müssen wir auch die Eigenschaft constructor einrichten, da wir den ursprünglichen Instanzprototyp ersetzt haben, wo er den korrekten Wert hatte.

Abbildung 17-6 zeigt, wie Sub und Super nun zusammenhängen. Subs Struktur ähnelt tatsächlich dem, was ich in Abbildung 17-5 skizziert habe. Das Diagramm zeigt nicht die Instanzeigenschaften, die durch den im Diagramm erwähnten Funktionsaufruf eingerichtet werden.

Sicherstellen, dass instanceof funktioniert

„Sicherstellen, dass instanceof funktioniert“ bedeutet, dass jede Instanz von Sub auch eine Instanz von Super sein muss. Abbildung 17-7 zeigt, wie die Prototypenkette von subInstance, einer Instanz von Sub, aussieht: Ihr erster Prototyp ist Sub.prototype und ihr zweiter Prototyp ist Super.prototype.

Beginnen wir mit einer einfacheren Frage: Ist subInstance eine Instanz von Sub? Ja, das ist es, denn die folgenden beiden Aussagen sind äquivalent (die letztere kann als Definition der ersteren betrachtet werden).

subInstance instanceof Sub
Sub.prototype.isPrototypeOf(subInstance)

Wie bereits erwähnt, ist Sub.prototype einer der Prototypen von subInstance, daher sind beide Aussagen wahr. Ebenso ist subInstance auch eine Instanz von Super, denn die folgenden beiden Aussagen gelten:

subInstance instanceof Super
Super.prototype.isPrototypeOf(subInstance)

Eine Methode überschreiben

Wir überschreiben eine Methode in Super.prototype, indem wir eine Methode mit demselben Namen zu Sub.prototype hinzufügen. methodB ist ein Beispiel, und in Abbildung 17-7 sehen wir, warum es funktioniert: Die Suche nach methodB beginnt in subInstance und findet Sub.prototype.methodB vor Super.prototype.methodB.

Einen Super-Aufruf tätigen

Um Super-Aufrufe zu verstehen, müssen Sie den Begriff Home-Objekt kennen. Das Home-Objekt einer Methode ist das Objekt, das die Eigenschaft besitzt, deren Wert die Methode ist. Zum Beispiel ist das Home-Objekt von Sub.prototype.methodB Sub.prototype. Das Super-Aufrufen einer Methode foo beinhaltet drei Schritte:

  1. Beginnen Sie Ihre Suche "nach" (im Prototyp von) dem Home-Objekt der aktuellen Methode.
  2. Suchen Sie nach einer Methode, deren Name foo ist.
  3. Rufen Sie diese Methode mit dem aktuellen this auf. Die Begründung ist, dass die Supermethode mit derselben Instanz wie die aktuelle Methode arbeiten muss; sie muss auf dieselben Instanzeigenschaften zugreifen können.

Daher sieht der Code der Submethode wie folgt aus. Sie ruft sich selbst per Super-Aufruf auf, sie ruft die Methode auf, die sie überschrieben hat.

Sub.prototype.methodB = function (x, y) {
    var superResult = Super.prototype.methodB.call(this, x, y); // (1)
    return this.prop3 + ' ' + superResult;
}

Eine Möglichkeit, den Super-Aufruf unter (1) zu lesen, ist wie folgt: Beziehen Sie sich direkt auf die Supermethode und rufen Sie sie mit dem aktuellen this auf. Wenn wir sie jedoch in drei Teile aufteilen, finden wir die oben genannten Schritte:

  1. Super.prototype: Beginnen Sie Ihre Suche in Super.prototype, dem Prototyp von Sub.prototype (dem Home-Objekt der aktuellen Methode Sub.prototype.methodB).
  2. methodB: Suchen Sie nach einer Methode mit dem Namen methodB.
  3. call(this, ...): Rufen Sie die im vorherigen Schritt gefundene Methode auf und behalten Sie das aktuelle this bei.

Vermeiden Sie das Festkodieren des Namens des Superkonstruktors

Bis jetzt haben wir immer auf Supermethoden und Superkonstruktoren Bezug genommen, indem wir den Namen des Superkonstruktors genannt haben. Diese Art der Festkodierung macht Ihren Code weniger flexibel. Sie können sie vermeiden, indem Sie den Superprototyp einer Eigenschaft von Sub zuweisen:

Sub._super = Super.prototype;

Dann sehen der Aufruf des Superkonstruktors und einer Supermethode wie folgt aus:

function Sub(prop1, prop2, prop3, prop4) {
    Sub._super.constructor.call(this, prop1, prop2);
    this.prop3 = prop3;
    this.prop4 = prop4;
}
Sub.prototype.methodB = function (x, y) {
    var superResult = Sub._super.methodB.call(this, x, y);
    return this.prop3 + ' ' + superResult;
}

Das Einrichten von Sub._super wird normalerweise von einer Hilfsfunktion gehandhabt, die auch den Subprototyp mit dem Superprototyp verbindet. Zum Beispiel:

function subclasses(SubC, SuperC) {
    var subProto = Object.create(SuperC.prototype);
    // Save `constructor` and, possibly, other methods
    copyOwnPropertiesFrom(subProto, SubC.prototype);
    SubC.prototype = subProto;
    SubC._super = SuperC.prototype;
};

Dieser Code verwendet die Hilfsfunktion copyOwnPropertiesFrom(), die in Copying an Object gezeigt und erklärt wird.

Tipp

Lesen Sie "subclasses" als Verb: SubC subclasses SuperC. Eine solche Hilfsfunktion kann einige der Mühen bei der Erstellung eines Subkonstruktors abnehmen: Es gibt weniger manuell zu tun, und der Name des Superkonstruktors wird nie redundant erwähnt. Das folgende Beispiel zeigt, wie es den Code vereinfacht.

Beispiel: Konstruktorerbschaft in Aktion

Als konkretes Beispiel nehmen wir an, dass der Konstruktor Person bereits existiert:

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return 'Person called '+this.name;
};

Wir möchten nun den Konstruktor Employee als Unterkonstruktor von Person erzeugen. Dies geschieht manuell und sieht wie folgt aus:

function Employee(name, title) {
    Person.call(this, name);
    this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.describe = function () {
    return Person.prototype.describe.call(this)+' ('+this.title+')';
};

Hier ist die Interaktion

> var jane = new Employee('Jane', 'CTO');
> jane.describe()
Person called Jane (CTO)
> jane instanceof Employee
true
> jane instanceof Person
true

Die Hilfsfunktion subclasses() aus dem vorherigen Abschnitt vereinfacht den Code von Employee etwas und vermeidet die harte Kodierung des Superkonstruktors Person.

function Employee(name, title) {
    Employee._super.constructor.call(this, name);
    this.title = title;
}
Employee.prototype.describe = function () {
    return Employee._super.describe.call(this)+' ('+this.title+')';
};
subclasses(Employee, Person);

Beispiel: Die Vererbungshierarchie von integrierten Konstruktoren

Integrierte Konstruktoren verwenden denselben Unterklassenansatz, der in diesem Abschnitt beschrieben wird. Beispielsweise ist Array ein Unterkonstruktor von Object. Daher sieht die Prototypenkette einer Instanz von Array wie folgt aus:

> var p = Object.getPrototypeOf

> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true

Antipön: Der Prototyp ist eine Instanz des Superkonstruktors

Vor ECMAScript 5 und Object.create() war eine häufig verwendete Lösung, den Unterprototyp zu erstellen, indem der Superkonstruktor aufgerufen wurde:

Sub.prototype = new Super();  // Don’t do this

Dies wird unter ECMAScript 5 nicht empfohlen. Der Prototyp wird alle Instanzeigenschaften von Super haben, die er nicht benötigt. Daher ist es besser, das oben erwähnte Muster (mit Object.create()) zu verwenden.

Methoden aller Objekte

Fast alle Objekte haben Object.prototype in ihrer Prototypenkette:

> Object.prototype.isPrototypeOf({})
true
> Object.prototype.isPrototypeOf([])
true
> Object.prototype.isPrototypeOf(/xyz/)
true

Die folgenden Unterabschnitte beschreiben die Methoden, die Object.prototype für seine Prototypen bereitstellt.

Umwandlung in ein Primitiv

Die folgenden beiden Methoden werden verwendet, um ein Objekt in einen primitiven Wert umzuwandeln:

Object.prototype.toString()

Gibt eine Zeichenfolgendarstellung eines Objekts zurück:

> ({ first: 'John', last: 'Doe' }.toString())
'[object Object]'
> [ 'a', 'b', 'c' ].toString()
'a,b,c'
Object.prototype.valueOf()

Dies ist der bevorzugte Weg, ein Objekt in eine Zahl umzuwandeln. Die Standardimplementierung gibt this zurück.

> var obj = {};
> obj.valueOf() === obj
true

valueOf wird von Wrapper-Konstruktoren überschrieben, um das gewrappte Primitiv zurückzugeben.

> new Number(7).valueOf()
7

Die Umwandlung in Zahl und Zeichenfolge (ob implizit oder explizit) baut auf der Umwandlung in ein Primitiv auf (Details siehe Algorithmus: ToPrimitive()-Umwandlung eines Wertes in ein Primitiv). Deshalb können Sie die oben genannten beiden Methoden verwenden, um diese Umwandlungen zu konfigurieren. valueOf() wird von der Umwandlung in eine Zahl bevorzugt.

> 3 * { valueOf: function () { return 5 } }
15

toString() wird von der Umwandlung in eine Zeichenfolge bevorzugt.

> String({ toString: function () { return 'ME' } })
'Result: ME'

Die Umwandlung in einen booleschen Wert ist nicht konfigurierbar; Objekte werden immer als true betrachtet (siehe Umwandlung in Boolesch).

Prototypische Vererbung und Eigenschaften

Die folgenden Methoden helfen bei der prototypischen Vererbung und Eigenschaften:

Object.prototype.isPrototypeOf(obj)

Gibt true zurück, wenn der Empfänger Teil der Prototypenkette von obj ist.

> var proto = { };
> var obj = Object.create(proto);
> proto.isPrototypeOf(obj)
true
> obj.isPrototypeOf(obj)
false
Object.prototype.hasOwnProperty(key)

Gibt true zurück, wenn this eine Eigenschaft mit dem Schlüssel key besitzt. „Eigen“ bedeutet, dass die Eigenschaft im Objekt selbst und nicht in einem seiner Prototypen vorhanden ist.

Warnung

Normalerweise sollten Sie diese Methode generisch aufrufen (nicht direkt), insbesondere für Objekte, deren Eigenschaften Sie nicht statisch kennen. Warum und wie, wird in Iteration und Erkennung von Eigenschaften erklärt.

> var proto = { foo: 'abc' };
> var obj = Object.create(proto);
> obj.bar = 'def';

> Object.prototype.hasOwnProperty.call(obj, 'foo')
false
> Object.prototype.hasOwnProperty.call(obj, 'bar')
true
Object.prototype.propertyIsEnumerable(propKey)

Gibt true zurück, wenn der Empfänger eine Eigenschaft mit dem Schlüssel propKey hat, die aufzählbar ist, und andernfalls false:

> var obj = { foo: 'abc' };
> obj.propertyIsEnumerable('foo')
true
> obj.propertyIsEnumerable('toString')
false
> obj.propertyIsEnumerable('unknown')
false

Generische Methoden: Ausleihen von Methoden aus Prototypen

Manchmal haben Instanzprototypen Methoden, die für mehr Objekte nützlich sind als die, die von ihnen erben. Dieser Abschnitt erklärt, wie die Methoden eines Prototyps verwendet werden können, ohne von ihm zu erben. Zum Beispiel hat der Instanzprototyp Wine.prototype die Methode incAge():

function Wine(age) {
    this.age = age;
}
Wine.prototype.incAge = function (years) {
    this.age += years;
}

Die Interaktion sieht wie folgt aus:

> var chablis = new Wine(3);
> chablis.incAge(1);
> chablis.age
4

Die Methode incAge() funktioniert für jedes Objekt, das die Eigenschaft age hat. Wie können wir sie für ein Objekt aufrufen, das keine Instanz von Wine ist? Schauen wir uns den vorherigen Methodenaufruf an:

chablis.incAge(1)

Es gibt tatsächlich zwei Argumente:

Wir können das Erste nicht durch ein beliebiges Objekt ersetzen – der Empfänger muss eine Instanz von Wine sein. Andernfalls wird die Methode incAge nicht gefunden. Aber der vorherige Methodenaufruf ist äquivalent zu (siehe auch Aufrufen von Funktionen unter Festlegung von this: call(), apply() und bind()):

Wine.prototype.incAge.call(chablis, 1)

Mit dem vorherigen Muster können wir ein Objekt zum Empfänger machen (erstes Argument von call), das keine Instanz von Wine ist, da der Empfänger nicht zum Finden der Methode Wine.prototype.incAge verwendet wird. Im folgenden Beispiel wenden wir die Methode incAge() auf das Objekt john an:

> var john = { age: 51 };
> Wine.prototype.incAge.call(john, 3)
> john.age
54

Eine Funktion, die auf diese Weise verwendet werden kann, wird als generische Methode bezeichnet; sie muss darauf vorbereitet sein, dass this keine Instanz „ihres“ Konstruktors ist. Daher sind nicht alle Methoden generisch; die ECMAScript-Sprachspezifikation gibt explizit an, welche dies sind (siehe Eine Liste aller generischen Methoden).

Dies sind einige Beispiele für generische Methoden in Aktion:

  • Verwenden Sie apply() (siehe Function.prototype.apply(thisValue, argArray)), um ein Array zu pushen (anstelle einzelner Elemente; siehe Hinzufügen und Entfernen von Elementen (destruktiv)).

    > var arr1 = [ 'a', 'b' ];
    > var arr2 = [ 'c', 'd' ];
    
    > [].push.apply(arr1, arr2)
    4
    > arr1
    [ 'a', 'b', 'c', 'd' ]

    Dieses Beispiel dreht sich darum, ein Array in Argumente zu verwandeln, nicht darum, eine Methode von einem anderen Konstruktor auszuleihen.

  • Wenden Sie die Array-Methode join() auf einen String an (der kein Array ist).

    > Array.prototype.join.call('abc', '-')
    'a-b-c'
  • Wenden Sie die Array-Methode map() auf einen String an:[17]

    > [].map.call('abc', function (x) { return x.toUpperCase() })
    [ 'A', 'B', 'C' ]

    Die generische Verwendung von map() ist effizienter als die Verwendung von split(''), was ein Zwischenarray erstellt.

    > 'abc'.split('').map(function (x) { return x.toUpperCase() })
    [ 'A', 'B', 'C' ]
  • Wenden Sie eine String-Methode auf Nicht-Strings an. toUpperCase() wandelt den Empfänger in eine Zeichenfolge um und wandelt das Ergebnis in Großbuchstaben um.

    > String.prototype.toUpperCase.call(true)
    'TRUE'
    > String.prototype.toUpperCase.call(['a','b','c'])
    'A,B,C'

Die generische Verwendung von Array-Methoden auf einfachen Objekten gibt Einblick in deren Funktionsweise.

  • Rufen Sie eine Array-Methode für ein Array-ähnliches Objekt auf.

    > var fakeArray = { 0: 'a', 1: 'b', length: 2 };
    > Array.prototype.join.call(fakeArray, '-')
    'a-b'
  • Sehen Sie, wie eine Array-Methode ein Objekt transformiert, das sie wie ein Array behandelt.

    > var obj = {};
    > Array.prototype.push.call(obj, 'hello');
    1
    > obj
    { '0': 'hello', length: 1 }

Array-ähnliche Objekte und generische Methoden

Es gibt einige Objekte in JavaScript, die sich wie ein Array anfühlen, aber tatsächlich keins sind. Das bedeutet, dass sie zwar indizierten Zugriff und eine length-Eigenschaft haben, aber keine der Array-Methoden (forEach(), push, concat() usw.). Das ist bedauerlich, aber wie wir sehen werden, ermöglichen generische Array-Methoden eine Lösung.

  • Die spezielle Variable arguments (siehe Alle Parameter nach Index: Die spezielle Variable arguments), die ein wichtiges Array-ähnliches Objekt ist, da sie ein grundlegender Bestandteil von JavaScript ist. arguments sieht aus wie ein Array:

    > function args() { return arguments }
    > var arrayLike = args('a', 'b');
    
    > arrayLike[0]
    'a'
    > arrayLike.length
    2

    Aber keine der Array-Methoden ist verfügbar.

    > arrayLike.join('-')
    TypeError: object has no method 'join'

    Das liegt daran, dass arrayLike keine Instanz von Array ist (und Array.prototype nicht in der Prototypenkette liegt).

    > arrayLike instanceof Array
    false
  • Browser DOM-Knotenlisten, die von document.getElementsBy*() (z. B. getElementsByTagName()), document.forms usw. zurückgegeben werden.

    > var elts = document.getElementsByTagName('h3');
    > elts.length
    3
    > elts instanceof Array
    false
  • Strings, die ebenfalls Array-ähnlich sind.

    > 'abc'[1]
    'b'
    > 'abc'.length
    3

Der Begriff Array-ähnlich kann auch als Vertrag zwischen generischen Array-Methoden und Objekten betrachtet werden. Die Objekte müssen bestimmte Anforderungen erfüllen, andernfalls funktionieren die Methoden nicht für sie. Die Anforderungen sind:

  • Die Elemente eines Array-ähnlichen Objekts müssen über eckige Klammern und ganzzahlige Indizes ab 0 zugänglich sein. Alle Methoden benötigen Lesezugriff, und einige Methoden benötigen zusätzlich Schreibzugriff. Beachten Sie, dass alle Objekte diese Art von Indizierung unterstützen: Ein Index in Klammern wird in eine Zeichenfolge umgewandelt und als Schlüssel verwendet, um einen Eigenschaftswert nachzuschlagen.

    > var obj = { '0': 'abc' };
    > obj[0]
    'abc'
  • Ein Array-ähnliches Objekt muss eine length-Eigenschaft haben, deren Wert die Anzahl seiner Elemente ist. Einige Methoden erfordern, dass length veränderbar ist (z. B. reverse()). Werte, deren Längen unveränderlich sind (z. B. Strings), können mit diesen Methoden nicht verwendet werden.

Muster für die Arbeit mit Array-ähnlichen Objekten

Die folgenden Muster sind für die Arbeit mit Array-ähnlichen Objekten nützlich:

Eine Liste aller generischen Methoden

Die folgende Liste enthält alle Methoden, die gemäß der ECMAScript-Sprachspezifikation generisch sind:

  • Array.prototype (siehe Array-Prototypmethoden)

    • concat
    • every
    • filter
    • forEach
    • indexOf
    • join
    • lastIndexOf
    • map
    • pop
    • push
    • reduce
    • reduceRight
    • reverse
    • shift
    • slice
    • some
    • sort
    • splice
    • toLocaleString
    • toString
    • unshift
  • Date.prototype (siehe Date-Prototypmethoden)

    • toJSON
  • Object.prototype (siehe Methoden aller Objekte)

    • (Alle Object-Methoden sind automatisch generisch – sie müssen für alle Objekte funktionieren.)
  • String.prototype (siehe String-Prototypmethoden)

    • charAt
    • charCodeAt
    • concat
    • indexOf
    • lastIndexOf
    • localeCompare
    • match
    • replace
    • search
    • slice
    • split
    • substring
    • toLocaleLowerCase
    • toLocaleUpperCase
    • toLowerCase
    • toUpperCase
    • trim

Fallstricke: Verwendung eines Objekts als Map

Da JavaScript keine integrierte Datenstruktur für Maps hat, werden Objekte oft als Maps von Zeichenfolgen zu Werten verwendet. Leider ist dies fehleranfälliger, als es scheint. Dieser Abschnitt erklärt drei Fallstricke, die bei dieser Aufgabe auftreten.

Die Operationen, die Eigenschaften lesen, können in zwei Arten unterteilt werden:

Sie müssen sorgfältig zwischen diesen Arten von Operationen wählen, wenn Sie die Einträge eines Objekt-als-Map lesen. Um zu verstehen, warum, betrachten wir das folgende Beispiel:

var proto = { protoProp: 'a' };
var obj = Object.create(proto);
obj.ownProp = 'b';

obj ist ein Objekt mit einer eigenen Eigenschaft, deren Prototyp proto ist, der ebenfalls eine eigene Eigenschaft hat. proto hat den Prototyp Object.prototype, wie alle Objekte, die durch Objektliterale erstellt werden. Daher erbt obj Eigenschaften sowohl von proto als auch von Object.prototype.

Wir möchten, dass obj als Map mit dem einzelnen Eintrag interpretiert wird:

ownProp: 'b'

Das heißt, wir möchten geerbte Eigenschaften ignorieren und nur eigene Eigenschaften berücksichtigen. Sehen wir uns an, welche Lesevorgänge obj auf diese Weise interpretieren und welche nicht. Beachten Sie, dass wir für Objekte-als-Maps normalerweise beliebige Eigenschaftsschlüssel verwenden möchten, die in Variablen gespeichert sind. Das schließt die Punktnotation aus.

Fallstrick 3: Die spezielle Eigenschaft __proto__

In vielen JavaScript-Engines ist die Eigenschaft __proto__ (siehe Die spezielle Eigenschaft __proto__) besonders: Das Abrufen ermittelt den Prototyp eines Objekts, und das Setzen ändert den Prototyp eines Objekts. Deshalb kann das Objekt keine Kartendaten in einer Eigenschaft speichern, deren Schlüssel '__proto__' ist. Wenn Sie den Map-Schlüssel '__proto__' zulassen möchten, müssen Sie ihn maskieren, bevor Sie ihn als Eigenschaftsschlüssel verwenden:

function get(obj, key) {
    return obj[escapeKey(key)];
}
function set(obj, key, value) {
    obj[escapeKey(key)] = value;
}
// Similar: checking if key exists, deleting an entry

function escapeKey(key) {
    if (key.indexOf('__proto__') === 0) {  // (1)
        return key+'%';
    } else {
        return key;
    }
}

Wir müssen auch die maskierte Version von '__proto__' (usw.) maskieren, um Kollisionen zu vermeiden; das heißt, wenn wir den Schlüssel '__proto__' als '__proto__%' maskieren, müssen wir auch den Schlüssel '__proto__%' maskieren, damit er keinen '__proto__'-Eintrag ersetzt. Das ist es, was in Zeile (1) passiert.

Mark S. Miller erwähnt die realen Auswirkungen dieses Fallstricks in einer E-Mail.

Denken Sie, diese Übung ist akademisch und tritt nicht in realen Systemen auf? Wie in einem Support-Thread beobachtet, hing bis vor kurzem auf allen Nicht-IE-Browsern, wenn Sie „__proto__“ am Anfang eines neuen Google Docs tippten, Ihr Google Doc. Dies wurde auf eine solche fehlerhafte Verwendung eines Objekts als Zeichenfolgen-Map zurückgeführt.

Das Dict-Muster: Objekte ohne Prototypen sind bessere Maps

Sie erstellen ein Objekt ohne Prototyp wie folgt:

var dict = Object.create(null);

Ein solches Objekt ist eine bessere Map (Wörterbuch) als ein normales Objekt, weshalb dieses Muster manchmal als Dict-Muster (dict für dictionary) bezeichnet wird. Untersuchen wir zunächst normale Objekte und finden dann heraus, warum Prototyp-lose Objekte bessere Maps sind.

Cheat Sheet: Arbeiten mit Objekten

Dieser Abschnitt ist eine schnelle Referenz mit Verweisen auf ausführlichere Erklärungen.



[17] Die Verwendung von map() auf diese Weise ist ein Tipp von Brandon Benvie (@benvie).

Weiter: 18. Arrays