instanceof-OperatorIn diesem Buch wird der objektorientierte Programmierstil (OOP) von JavaScript in vier Schritten vorgestellt. Dieses Kapitel behandelt die Schritte 2–4, das vorherige Kapitel behandelt Schritt 1. Die Schritte sind (Abb. 9)
Prototypen sind der einzige Vererbungsmechanismus von JavaScript: Jedes Objekt hat einen Prototyp, der entweder null oder ein Objekt ist. Im letzteren Fall erbt das Objekt alle Eigenschaften des Prototyps.
In einem Objekt-Literal kann der Prototyp über die spezielle Eigenschaft __proto__ gesetzt werden.
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);Da ein Prototypobjekt selbst einen Prototyp haben kann, erhalten wir eine Kette von Objekten – die sogenannte *Prototypkette*. Das bedeutet, dass uns die Vererbung den Eindruck vermittelt, wir hätten es mit einzelnen Objekten zu tun, tatsächlich haben wir es aber mit Ketten von Objekten zu tun.
Abb. 10 zeigt, wie die Prototypkette von obj aussieht.
Nicht geerbte Eigenschaften werden als *eigene Eigenschaften* bezeichnet. obj hat eine eigene Eigenschaft, .objProp.
Einige Operationen berücksichtigen alle Eigenschaften (eigene und geerbte) – zum Beispiel das Abrufen von Eigenschaften.
> const obj = { foo: 1 };
> typeof obj.foo // own
'number'
> typeof obj.toString // inherited
'function'Andere Operationen berücksichtigen nur eigene Eigenschaften – zum Beispiel Object.keys().
> Object.keys(obj)
[ 'foo' ]Lesen Sie weiter für eine weitere Operation, die ebenfalls nur eigene Eigenschaften berücksichtigt: das Setzen von Eigenschaften.
Ein Aspekt von Prototypketten, der kontraintuitiv sein mag, ist, dass das Setzen *irgendeiner* Eigenschaft über ein Objekt – selbst einer geerbten – nur dieses eine Objekt verändert – niemals eines der Prototypen.
Betrachten Sie das folgende Objekt obj:
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};Im nächsten Code-Snippet setzen wir die geerbte Eigenschaft obj.protoProp (Zeile A). Das "verändert" sie, indem eine eigene Eigenschaft erstellt wird: Beim Lesen von obj.protoProp wird zuerst die eigene Eigenschaft gefunden und ihr Wert überschreibt den Wert der geerbten Eigenschaft.
// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);
obj.protoProp = 'x'; // (A)
// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');
// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');Die Prototypkette von obj ist in Abb. 11 dargestellt.
__proto__, außer in Objekt-LiteralenIch empfehle, die Pseudo-Eigenschaft __proto__ zu vermeiden: Wie wir später sehen werden, haben nicht alle Objekte sie.
In Objekt-Literalen ist __proto__ jedoch anders. Dort ist es ein eingebautes Feature und immer verfügbar.
Die empfohlenen Wege, Prototypen abzurufen und zu setzen, sind:
Der beste Weg, einen Prototyp abzurufen, ist über die folgende Methode:
Object.getPrototypeOf(obj: Object) : ObjectDer beste Weg, einen Prototyp zu setzen, ist bei der Erstellung eines Objekts – über __proto__ in einem Objekt-Literal oder über
Object.create(proto: Object) : ObjectWenn Sie müssen, können Sie Object.setPrototypeOf() verwenden, um den Prototyp eines vorhandenen Objekts zu ändern. Dies kann sich jedoch negativ auf die Leistung auswirken.
So werden diese Features verwendet:
const proto1 = {};
const proto2 = {};
const obj = Object.create(proto1);
assert.equal(Object.getPrototypeOf(obj), proto1);
Object.setPrototypeOf(obj, proto2);
assert.equal(Object.getPrototypeOf(obj), proto2);Bisher bedeutete "p ist ein Prototyp von o" immer "p ist ein *direkter* Prototyp von o". Aber es kann auch lockerer verwendet werden und bedeuten, dass p in der Prototypkette von o liegt. Diese lockerere Beziehung kann über folgenden Ausdruck überprüft werden:
p.isPrototypeOf(o)Zum Beispiel
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);Betrachten Sie den folgenden Code
const jane = {
name: 'Jane',
describe() {
return 'Person named '+this.name;
},
};
const tarzan = {
name: 'Tarzan',
describe() {
return 'Person named '+this.name;
},
};
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');Wir haben zwei sehr ähnliche Objekte. Beide haben zwei Eigenschaften, deren Namen .name und .describe sind. Außerdem ist die Methode .describe() dieselbe. Wie können wir diese Methode duplizieren?
Wir können sie in ein Objekt PersonProto verschieben und dieses Objekt zum Prototyp von jane und tarzan machen.
const PersonProto = {
describe() {
return 'Person named ' + this.name;
},
};
const jane = {
__proto__: PersonProto,
name: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
name: 'Tarzan',
};Der Name des Prototyps spiegelt wider, dass sowohl jane als auch tarzan Personen sind.
Abb. 12 veranschaulicht, wie die drei Objekte verbunden sind: Die Objekte unten enthalten nun die spezifischen Eigenschaften für jane und tarzan. Das Objekt oben enthält die Eigenschaften, die zwischen ihnen geteilt werden.
Wenn Sie den Methodenaufruf jane.describe() tätigen, zeigt this auf den Empfänger dieses Methodenaufrufs, jane (in der unteren linken Ecke des Diagramms). Deshalb funktioniert die Methode immer noch. tarzan.describe() funktioniert ähnlich.
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');Wir sind nun bereit, uns mit Klassen zu beschäftigen, die im Grunde eine kompakte Syntax zum Einrichten von Prototypketten sind. Unter der Haube sind die Klassen von JavaScript unkonventionell. Aber das sieht man selten, wenn man mit ihnen arbeitet. Sie sollten sich normalerweise für Personen, die andere objektorientierte Programmiersprachen verwendet haben, vertraut anfühlen.
Wir haben uns zuvor mit jane und tarzan beschäftigt, einzelnen Objekten, die Personen repräsentieren. Verwenden wir eine *Klassendeklaration*, um eine Fabrik für Personenobjekte zu implementieren:
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}jane und tarzan können jetzt über new Person() erstellt werden.
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');
const tarzan = new Person('Tarzan');
assert.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan');Die Klasse Person hat zwei Methoden:
.describe()..constructor(), die direkt nach der Erstellung einer neuen Instanz aufgerufen wird und diese Instanz initialisiert. Sie erhält die Argumente, die an den new-Operator übergeben werden (nach dem Klassennamen). Wenn Sie keine Argumente zum Einrichten einer neuen Instanz benötigen, können Sie den Konstruktor weglassen.Es gibt zwei Arten von *Klassendefinitionen* (Wege, Klassen zu definieren):
Klassenausdrücke können anonym und benannt sein.
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };Der Name eines benannten Klassenausdrucks funktioniert ähnlich wie der Name eines benannten Funktionsausdrucks.
Dies war ein erster Überblick über Klassen. Wir werden bald weitere Features untersuchen, müssen aber zuerst die Interna von Klassen lernen.
Unter der Haube von Klassen geschieht viel. Betrachten wir das Diagramm für jane (Abb. 13).
Der Hauptzweck der Klasse Person ist die Einrichtung der Prototypkette auf der rechten Seite (jane, gefolgt von Person.prototype). Interessant ist, dass beide Konstrukte innerhalb der Klasse Person (.constructor und .describe()) Eigenschaften für Person.prototype, nicht für Person, erstellt haben.
Der Grund für diesen etwas eigenartigen Ansatz ist die Rückwärtskompatibilität: Vor Klassen wurden *Konstruktorfunktionen* (gewöhnliche Funktionen, aufgerufen über den new-Operator) oft als Fabriken für Objekte verwendet. Klassen sind meist eine bessere Syntax für Konstruktorfunktionen und bleiben daher mit altem Code kompatibel. Das erklärt, warum Klassen Funktionen sind.
> typeof Person
'function'In diesem Buch verwende ich die Begriffe *Konstruktor (Funktion)* und *Klasse* synonym.
Es ist leicht, .__proto__ und .prototype zu verwechseln. Hoffentlich macht Abb. 13 klar, wie sie sich unterscheiden.
.__proto__ ist eine Pseudo-Eigenschaft zum Zugriff auf den Prototyp eines Objekts..prototype ist eine normale Eigenschaft, die nur besonders ist, da der new-Operator sie verwendet. Der Name ist nicht ideal: Person.prototype zeigt nicht auf den Prototyp von Person, sondern auf den Prototyp aller Instanzen von Person.Person.prototype.constructor (fortgeschritten)Es gibt ein Detail in Abb. 13, das wir noch nicht betrachtet haben: Person.prototype.constructor zeigt zurück auf Person.
> Person.prototype.constructor === Person
trueDiese Einrichtung existiert ebenfalls aufgrund der Rückwärtskompatibilität. Aber sie hat zwei zusätzliche Vorteile.
Erstens erbt jede Instanz einer Klasse die Eigenschaft .constructor. Daher können Sie damit, ausgehend von einer Instanz, "ähnliche" Objekte erstellen:
const jane = new Person('Jane');
const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
// (the instanceof operator is explained later)
assert.equal(cheeta instanceof Person, true);Zweitens können Sie den Namen der Klasse erhalten, die eine gegebene Instanz erstellt hat.
const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');Alle Konstrukte im Körper der folgenden Klassendeklaration erstellen Eigenschaften von Foo.prototype.
class Foo {
constructor(prop) {
this.prop = prop;
}
protoMethod() {
return 'protoMethod';
}
get protoGetter() {
return 'protoGetter';
}
}Betrachten wir sie der Reihe nach:
.constructor() wird nach der Erstellung einer neuen Instanz von Foo aufgerufen, um diese Instanz einzurichten..protoMethod() ist eine normale Methode. Sie wird in Foo.prototype gespeichert..protoGetter ist ein Getter, der in Foo.prototype gespeichert wird.Die folgende Interaktion verwendet die Klasse Foo:
> const foo = new Foo(123);
> foo.prop
123
> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'Alle Konstrukte im Körper der folgenden Klassendeklaration erstellen sogenannte *statische* Eigenschaften – Eigenschaften von Bar selbst.
class Bar {
static staticMethod() {
return 'staticMethod';
}
static get staticGetter() {
return 'staticGetter';
}
}Die statische Methode und der statische Getter werden wie folgt verwendet:
> Bar.staticMethod()
'staticMethod'
> Bar.staticGetter
'staticGetter'instanceof-OperatorDer instanceof-Operator teilt Ihnen mit, ob ein Wert eine Instanz einer gegebenen Klasse ist.
> new Person('Jane') instanceof Person
true
> ({}) instanceof Person
false
> ({}) instanceof Object
true
> [] instanceof Array
trueWir werden den instanceof-Operator später im Detail untersuchen, nachdem wir uns mit Unterklassenbildung beschäftigt haben.
Ich empfehle die Verwendung von Klassen aus folgenden Gründen:
Klassen sind ein gängiger Standard für die Objekterstellung und -vererbung, der jetzt weitgehend von Frameworks (React, Angular, Ember usw.) unterstützt wird. Das ist eine Verbesserung gegenüber früher, als fast jedes Framework seine eigene Vererbungsbibliothek hatte.
Sie helfen Werkzeugen wie IDEs und Typüberprüfungen bei ihrer Arbeit und ermöglichen dort neue Features.
Wenn Sie aus einer anderen Sprache zu JavaScript kommen und Klassen gewohnt sind, können Sie schneller loslegen.
JavaScript-Engines optimieren sie. Das heißt, Code, der Klassen verwendet, ist fast immer schneller als Code, der eine benutzerdefinierte Vererbungsbibliothek verwendet.
Sie können eingebaute Konstruktorfunktionen wie Error unterklassifizieren.
Das bedeutet nicht, dass Klassen perfekt sind:
Es besteht die Gefahr, Vererbung zu übertreiben.
Es besteht die Gefahr, zu viel Funktionalität in Klassen zu packen (wenn ein Teil davon besser in Funktionen aufgehoben wäre).
Wie sie oberflächlich und im Detail funktionieren, ist ziemlich unterschiedlich. Mit anderen Worten, es gibt eine Diskrepanz zwischen Syntax und Semantik. Zwei Beispiele sind:
C erstellt eine Methode im Objekt C.prototype.Die Motivation für die Diskrepanz ist die Rückwärtskompatibilität. Glücklicherweise verursacht die Diskrepanz in der Praxis wenige Probleme; Sie sind normalerweise in Ordnung, wenn Sie sich an das halten, was Klassen vorgeben zu sein.
Übung: Eine Klasse schreiben
exercises/proto-chains-classes/point_class_test.mjs
Dieser Abschnitt beschreibt Techniken zum Verbergen einiger Objektdaten vor der Außenwelt. Wir diskutieren sie im Kontext von Klassen, aber sie funktionieren auch für direkt erstellte Objekte, z. B. über Objekt-Literale.
Die erste Technik macht eine Eigenschaft privat, indem ihr Name mit einem Unterstrich präfigiert wird. Dies schützt die Eigenschaft in keiner Weise; es signalisiert nur nach außen: „Diese Eigenschaft müssen Sie nicht kennen.“
Im folgenden Code sind die Eigenschaften ._counter und ._action privat.
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
// The two properties aren’t really private:
assert.deepEqual(
Object.keys(new Countdown()),
['_counter', '_action']);Mit dieser Technik erhalten Sie keinen Schutz und private Namen können kollidieren. Auf der positiven Seite ist sie einfach zu verwenden.
Eine weitere Technik ist die Verwendung von WeakMaps. Wie genau das funktioniert, wird im Kapitel über WeakMaps erklärt. Dies ist eine Vorschau:
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);Diese Technik bietet erheblichen Schutz vor externem Zugriff und Namenskollisionen sind ausgeschlossen. Aber sie ist auch komplizierter zu verwenden.
Dieses Buch erklärt die wichtigsten Techniken für private Daten in Klassen. Wahrscheinlich wird es bald auch integrierte Unterstützung dafür geben. Konsultieren Sie den ECMAScript-Vorschlag „Class Public Instance Fields & Private Instance Fields“ für Details.
Einige zusätzliche Techniken werden in Exploring ES6 erklärt.
Klassen können auch bestehende Klassen unterklassifizieren ("erweitern"). Als Beispiel unterklassifiziert die folgende Klasse Employee die Klasse Person:
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');Zwei Kommentare:
Innerhalb einer .constructor()-Methode müssen Sie den Superkonstruktor über super() aufrufen, bevor Sie auf this zugreifen können. Das liegt daran, dass this erst existiert, nachdem der Superkonstruktor aufgerufen wurde (dieses Phänomen ist spezifisch für Klassen).
Statische Methoden werden ebenfalls vererbt. Zum Beispiel erbt Employee die statische Methode .logNames().
> 'logNames' in Employee
true Übung: Unterklassenbildung
exercises/proto-chains-classes/color_point_class_test.mjs
Die Klassen Person und Employee aus dem vorherigen Abschnitt bestehen aus mehreren Objekten (Abb. 14). Eine Schlüssel Erkenntnis zum Verständnis, wie diese Objekte zusammenhängen, ist, dass es zwei Prototypketten gibt:
Die Instanz-Prototypkette beginnt mit jane und setzt sich mit Employee.prototype und Person.prototype fort. Im Prinzip endet die Prototypkette an dieser Stelle, aber wir erhalten noch ein Objekt: Object.prototype. Dieser Prototyp stellt praktisch allen Objekten Dienste zur Verfügung, weshalb er hier auch enthalten ist.
> Object.getPrototypeOf(Person.prototype) === Object.prototype
trueIn der Klassen-Prototypkette kommt Employee zuerst, dann Person. Danach setzt sich die Kette mit Function.prototype fort, die nur vorhanden ist, weil Person eine Funktion ist und Funktionen die Dienste von Function.prototype benötigen.
> Object.getPrototypeOf(Person) === Function.prototype
trueinstanceof im Detail (fortgeschritten)Wir haben noch nicht gesehen, wie instanceof wirklich funktioniert. Angenommen, der Ausdruck:
x instanceof CWie bestimmt instanceof, ob x eine Instanz von C (oder einer Unterklasse von C) ist? Es tut dies, indem es prüft, ob C.prototype in der Prototypkette von x liegt. Das heißt, der folgende Ausdruck ist äquivalent:
C.prototype.isPrototypeOf(x)Wenn wir zu Abb. 14 zurückkehren, können wir bestätigen, dass die Prototypkette uns zu folgenden korrekten Antworten führt:
> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
trueAls Nächstes verwenden wir unser Wissen über Unterklassenbildung, um die Prototypketten einiger eingebauter Objekte zu verstehen. Die folgende Hilfsfunktion p() hilft uns bei unseren Erkundungen.
const p = Object.getPrototypeOf.bind(Object);Wir extrahierten die Methode .getPrototypeOf() von Object und wiesen sie p zu.
{}Beginnen wir mit der Untersuchung einfacher Objekte:
> p({}) === Object.prototype
true
> p(p({})) === null
trueAbb. 15 zeigt ein Diagramm für diese Prototypkette. Wir sehen, dass {} wirklich eine Instanz von Object ist – Object.prototype liegt in seiner Prototypkette.
[]Wie sieht die Prototypkette eines Arrays aus?
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
trueDiese Prototypkette (visualisiert in Abb. 16) besagt, dass ein Array-Objekt eine Instanz von Array ist, was eine Unterklasse von Object ist.
function () {}Zuletzt sagt uns die Prototypkette einer gewöhnlichen Funktion, dass alle Funktionen Objekte sind.
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
trueObject sindEin Objekt ist nur dann eine Instanz von Object, wenn Object.prototype in seiner Prototypkette liegt. Die meisten über verschiedene Literale erstellten Objekte sind Instanzen von Object.
> ({}) instanceof Object
true
> (() => {}) instanceof Object
true
> /abc/ug instanceof Object
trueObjekte, die keine Prototypen haben, sind keine Instanzen von Object.
> ({ __proto__: null }) instanceof Object
falseObject.prototype beendet die meisten Prototypketten. Sein Prototyp ist null, was bedeutet, dass es auch keine Instanz von Object ist.
> Object.prototype instanceof Object
false.__proto__?Die Pseudo-Eigenschaft .__proto__ wird von der Klasse Object über einen Getter und einen Setter implementiert. Sie könnte wie folgt implementiert werden:
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}Das bedeutet, dass Sie .__proto__ ausschalten können, indem Sie ein Objekt erstellen, das Object.prototype nicht in seiner Prototypkette hat (siehe vorheriger Abschnitt).
> '__proto__' in {}
true
> '__proto__' in { __proto__: null }
falseBetrachten wir, wie Methodenaufrufe mit Klassen funktionieren. Wir greifen wieder auf jane von früher zurück:
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
const jane = new Person('Jane');Abb. 17 enthält ein Diagramm mit der Prototypkette von jane.
Normale Methodenaufrufe werden *delegiert* – der Methodenaufruf jane.describe() erfolgt in zwei Schritten:
Delegation: Suchen Sie in der Prototypkette von jane nach der ersten Eigenschaft, deren Schlüssel 'describe' ist, und rufen Sie ihren Wert ab.
const func = jane.describe;Aufruf: Rufen Sie den Wert auf und setzen Sie dabei this auf jane.
func.call(jane);Diese dynamische Suche nach einer Methode und deren Aufruf wird als *dynamische Delegation* bezeichnet.
Sie können denselben Methodenaufruf auch *direkt* ohne Delegation ausführen:
Person.prototype.describe.call(jane)Diesmal zeigen wir direkt auf die Methode über Person.prototype.describe und suchen nicht in der Prototypkette danach. Wir spezifizieren this auch anders über .call().
Beachten Sie, dass this immer auf den Anfang einer Prototypkette zeigt. Dies ermöglicht es .describe(), auf .name zuzugreifen.
Direkte Methodenaufrufe werden nützlich, wenn Sie mit Methoden von Object.prototype arbeiten. Zum Beispiel prüft Object.prototype.hasOwnProperty(k), ob this eine nicht geerbte Eigenschaft mit dem Schlüssel k hat.
> const obj = { foo: 123 };
> obj.hasOwnProperty('foo')
true
> obj.hasOwnProperty('bar')
falseAllerdings kann es in der Prototypkette eines Objekts eine andere Eigenschaft mit dem Schlüssel 'hasOwnProperty' geben, die die Methode in Object.prototype überschreibt. Dann funktioniert ein delegierter Methodenaufruf nicht.
> const obj = { hasOwnProperty: true };
> obj.hasOwnProperty('bar')
TypeError: obj.hasOwnProperty is not a functionDie Lösung ist die Verwendung eines direkten Methodenaufrufs:
> Object.prototype.hasOwnProperty.call(obj, 'bar')
false
> Object.prototype.hasOwnProperty.call(obj, 'hasOwnProperty')
trueDiese Art von direktem Methodenaufruf wird oft wie folgt abgekürzt:
> ({}).hasOwnProperty.call(obj, 'bar')
false
> ({}).hasOwnProperty.call(obj, 'hasOwnProperty')
trueDieses Muster mag ineffizient erscheinen, aber die meisten Engines optimieren dieses Muster, sodass die Leistung kein Problem darstellen sollte.
Das Klassensystem von JavaScript unterstützt nur *einfache Vererbung*. Das heißt, jede Klasse kann höchstens eine Oberklasse haben. Ein Weg um diese Einschränkung ist eine Technik namens *Mixin-Klassen* (kurz: *Mixins*).
Die Idee ist folgende: Nehmen wir an, wir möchten, dass eine Klasse C von zwei Oberklassen S1 und S2 erbt. Das wäre *mehrfache Vererbung*, die JavaScript nicht unterstützt.
Unsere Lösung besteht darin, S1 und S2 in *Mixins* zu verwandeln, Fabriken für Unterklassen:
const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };Jede dieser beiden Funktionen gibt eine Klasse zurück, die eine gegebene Oberklasse Sup erweitert. Wir erstellen die Klasse C wie folgt:
class C extends S2(S1(Object)) {
/*···*/
}Wir haben nun eine Klasse C, die eine Klasse S2 erweitert, die eine Klasse S1 erweitert, die Object erweitert (was die meisten Klassen implizit tun).
Wir implementieren ein Mixin Branded, das Hilfsmethoden zum Setzen und Abrufen der Marke eines Objekts hat:
const Branded = (Sup) => class extends Sup {
setBrand(brand) {
this._brand = brand;
return this;
}
getBrand() {
return this._brand;
}
};Wir verwenden dieses Mixin, um das Markenmanagement für eine Klasse Car zu implementieren:
class Car extends Branded(Object) {
constructor(model) {
super();
this._model = model;
}
toString() {
return `${this.getBrand()} ${this._model}`;
}
}Der folgende Code bestätigt, dass das Mixin funktioniert hat: Car hat die Methode .setBrand() von Branded.
const modelT = new Car('Model T').setBrand('Ford');
assert.equal(modelT.toString(), 'Ford Model T');Mixins befreien uns von den Beschränkungen der einfachen Vererbung.
Prinzipiell sind Objekte ungeordnet. Der Hauptgrund für die Reihenfolge von Eigenschaften ist, dass Operationen, die Einträge, Schlüssel oder Werte auflisten, deterministisch sind. Das hilft z. B. beim Testen.
Quiz
Siehe Quiz-App.