JavaScript für ungeduldige Programmierer (ES2021-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

29 Prototypketten und Klassen



In 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)

  1. Einzelne Objekte (vorheriges Kapitel): Wie funktionieren *Objekte*, die grundlegenden Bausteine der objektorientierten Programmierung in JavaScript, isoliert?
  2. Prototypketten (dieses Kapitel): Jedes Objekt hat eine Kette von null oder mehr *Prototypobjekten*. Prototypen sind der Kernmechanismus für Vererbung in JavaScript.
  3. Klassen (dieses Kapitel): Die *Klassen* von JavaScript sind Fabriken für Objekte. Die Beziehung zwischen einer Klasse und ihren Instanzen basiert auf prototypischer Vererbung.
  4. Unterklassenbildung (dieses Kapitel): Die Beziehung zwischen einer *Unterklasse* und ihrer *Oberklasse* basiert ebenfalls auf prototypischer Vererbung.
Figure 9: This book introduces object-oriented programming in JavaScript in four steps.

29.1 Prototypketten

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.

Figure 10: obj starts a chain of objects that continues with proto and other objects.

Nicht geerbte Eigenschaften werden als *eigene Eigenschaften* bezeichnet. obj hat eine eigene Eigenschaft, .objProp.

29.1.1 JavaScript-Operationen: alle Eigenschaften vs. eigene Eigenschaften

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.

29.1.2 Fallstrick: Nur das erste Element einer Prototypkette wird verändert

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.

Figure 11: The own property .protoProp of obj overrides the property inherited from proto.

29.1.3 Tipps für die Arbeit mit Prototypen (fortgeschritten)

29.1.3.1 Best Practice: Vermeiden Sie __proto__, außer in Objekt-Literalen

Ich 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:

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);
29.1.3.2 Prüfen: Ist ein Objekt ein Prototyp eines anderen?

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);

29.1.4 Daten über Prototypen teilen

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.

Figure 12: Objects jane and tarzan share method .describe(), via their common prototype PersonProto.

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');

29.2 Klassen

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.

29.2.1 Eine Klasse für Personen

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:

29.2.1.1 Klassenausdrücke

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.

29.2.2 Klassen im Detail

Unter der Haube von Klassen geschieht viel. Betrachten wir das Diagramm für jane (Abb. 13).

Figure 13: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. jane is one such instance.

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.

29.2.2.1 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
true

Diese 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');

29.2.3 Klassendefinitionen: Prototyp-Eigenschaften

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:

Die folgende Interaktion verwendet die Klasse Foo:

> const foo = new Foo(123);
> foo.prop
123

> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'

29.2.4 Klassendefinitionen: statische Eigenschaften

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'

29.2.5 Der instanceof-Operator

Der 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
true

Wir werden den instanceof-Operator später im Detail untersuchen, nachdem wir uns mit Unterklassenbildung beschäftigt haben.

29.2.6 Warum ich Klassen empfehle

Ich empfehle die Verwendung von Klassen aus folgenden Gründen:

Das bedeutet nicht, dass Klassen perfekt sind:

  Übung: Eine Klasse schreiben

exercises/proto-chains-classes/point_class_test.mjs

29.3 Private Daten für Klassen

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.

29.3.1 Private Daten: Namenskonvention

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.

29.3.2 Private Daten: WeakMaps

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.

29.3.3 Weitere Techniken für private Daten

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.

29.4 Unterklassenbildung

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:

  Übung: Unterklassenbildung

exercises/proto-chains-classes/color_point_class_test.mjs

29.4.1 Unterklassen im Detail (fortgeschritten)

Figure 14: These are the objects that make up class Person and its subclass, Employee. The left column is about classes. The right column is about the Employee instance jane and its prototype chain.

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:

29.4.1.1 Die Instanz-Prototypkette (rechte Spalte)

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
true
29.4.1.2 Die Klassen-Prototypkette (linke Spalte)

In 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
true

29.4.2 instanceof im Detail (fortgeschritten)

Wir haben noch nicht gesehen, wie instanceof wirklich funktioniert. Angenommen, der Ausdruck:

x instanceof C

Wie 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
true

29.4.3 Prototypketten von eingebauten Objekten (fortgeschritten)

Als 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.

29.4.3.1 Die Prototypkette von {}

Beginnen wir mit der Untersuchung einfacher Objekte:

> p({}) === Object.prototype
true
> p(p({})) === null
true
Figure 15: The prototype chain of an object created via an object literal starts with that object, continues with Object.prototype, and ends with null.

Abb. 15 zeigt ein Diagramm für diese Prototypkette. Wir sehen, dass {} wirklich eine Instanz von Object ist – Object.prototype liegt in seiner Prototypkette.

29.4.3.2 Die Prototypkette von []

Wie sieht die Prototypkette eines Arrays aus?

> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true
Figure 16: The prototype chain of an Array has these members: the Array instance, Array.prototype, Object.prototype, null.

Diese Prototypkette (visualisiert in Abb. 16) besagt, dass ein Array-Objekt eine Instanz von Array ist, was eine Unterklasse von Object ist.

29.4.3.3 Die Prototypkette von 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
true
29.4.3.4 Objekte, die keine Instanzen von Object sind

Ein 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
true

Objekte, die keine Prototypen haben, sind keine Instanzen von Object.

> ({ __proto__: null }) instanceof Object
false

Object.prototype beendet die meisten Prototypketten. Sein Prototyp ist null, was bedeutet, dass es auch keine Instanz von Object ist.

> Object.prototype instanceof Object
false
29.4.3.5 Wie genau funktioniert die Pseudo-Eigenschaft .__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 }
false

29.4.4 Delegierte vs. direkte Methodenaufrufe (fortgeschritten)

Betrachten 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.

Figure 17: The prototype chain of jane starts with jane and continues with Person.prototype.

Normale Methodenaufrufe werden *delegiert* – der Methodenaufruf jane.describe() erfolgt in zwei Schritten:

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.

29.4.4.1 Methoden leihen

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')
false

Allerdings 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 function

Die Lösung ist die Verwendung eines direkten Methodenaufrufs:

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

Diese Art von direktem Methodenaufruf wird oft wie folgt abgekürzt:

> ({}).hasOwnProperty.call(obj, 'bar')
false
> ({}).hasOwnProperty.call(obj, 'hasOwnProperty')
true

Dieses Muster mag ineffizient erscheinen, aber die meisten Engines optimieren dieses Muster, sodass die Leistung kein Problem darstellen sollte.

29.4.5 Mixin-Klassen (fortgeschritten)

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).

29.4.5.1 Beispiel: Ein Mixin für Markenmanagement

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');
29.4.5.2 Die Vorteile von Mixins

Mixins befreien uns von den Beschränkungen der einfachen Vererbung.

29.5 FAQ: Objekte

29.5.1 Warum behalten Objekte die Einfügereihenfolge von Eigenschaften bei?

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.