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

29 Klassen [ES6]



In diesem Buch wird der objektorientierte Programmierstil (OOP) von JavaScript in vier Schritten eingeführt. Dieses Kapitel behandelt die Schritte 3 und 4, das vorherige Kapitel behandelt die Schritte 1 und 2. Die Schritte sind (Abb. 12)

  1. Einzelne Objekte (vorheriges Kapitel): Wie funktionieren Objekte, die grundlegenden OOP-Bausteine von JavaScript, isoliert?
  2. Prototypketten (vorheriges 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 (Schritt 2).
  4. Unterklassenbildung (dieses Kapitel): Die Beziehung zwischen einer Unterklasse und ihrer Oberklasse basiert ebenfalls auf prototypischer Vererbung.
Figure 12: This book introduces object-oriented programming in JavaScript in four steps.

29.1 Spickzettel: Klassen

Oberklasse

class Person {
  #firstName; // (A)
  constructor(firstName) {
    this.#firstName = firstName; // (B)
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}
const tarzan = new Person('Tarzan');
assert.equal(
  tarzan.describe(),
  'Person named Tarzan'
);
assert.deepEqual(
  Person.extractNames([tarzan, new Person('Cheeta')]),
  ['Tarzan', 'Cheeta']
);

Unterklasse

class Employee extends Person {
  constructor(firstName, title) {
    super(firstName);
    this.title = title; // (C)
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.title,
  'CTO'
);
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)'
);

Hinweise

29.2 Das Wesentliche über Klassen

Klassen sind im Grunde eine kompakte Syntax zum Einrichten von Prototypketten (die im vorherigen Kapitel erklärt werden). Unter der Haube sind die Klassen von JavaScript unkonventionell. Aber das ist etwas, das wir bei der Arbeit mit ihnen selten sehen. Sie sollten sich normalerweise für Leute, die andere objektorientierte Programmiersprachen verwendet haben, vertraut anfühlen.

Beachten Sie, dass wir keine Klassen benötigen, um Objekte zu erstellen. Wir können dies auch über Objektliterale tun. Deshalb ist das Singleton-Muster in JavaScript nicht notwendig und Klassen werden weniger verwendet als in vielen anderen Sprachen, die sie haben.

29.2.1 Eine Klasse für Personen

Wir haben zuvor mit jane und tarzan gearbeitet, einzelnen Objekten, die Personen repräsentieren. Verwenden wir eine Klassendeklaration, um eine Fabrik für solche Objekte zu implementieren

class Person {
  #firstName; // (A)
  constructor(firstName) {
    this.#firstName = firstName; // (B)
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}

jane und tarzan können jetzt über new Person() erstellt werden

const jane = new Person('Jane');
const tarzan = new Person('Tarzan');

Untersuchen wir, was sich im Körper der Klasse Person befindet.

Wir können auch Instanzeigenschaften (öffentliche Felder) in Konstruktoren erstellen

class Container {
  constructor(value) {
    this.value = value;
  }
}
const abcContainer = new Container('abc');
assert.equal(
  abcContainer.value, 'abc'
);

Im Gegensatz zu Instanz-privaten Feldern müssen Instanzeigenschaften nicht in Klassenkörpern deklariert werden.

29.2.2 Klassenausdrücke

Es gibt zwei Arten von Klassendefinitionen (Arten der Klassendefinition)

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: Er kann nur innerhalb des Körpers einer Klasse aufgerufen werden und bleibt derselbe, unabhängig davon, dem die Klasse zugewiesen wird.

29.2.3 Der instanceof-Operator

Der instanceof-Operator sagt uns, 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 detaillierter untersuchen, nachdem wir uns mit der Unterklassenbildung beschäftigt haben.

29.2.4 Öffentliche Slots (Eigenschaften) vs. private Slots

In der Programmiersprache JavaScript können Objekte zwei Arten von „Slots“ haben.

Das sind die wichtigsten Regeln, die wir über Eigenschaften und private Slots wissen müssen

  Mehr Informationen zu Eigenschaften und privaten Slots

Dieses Kapitel behandelt nicht alle Details zu Eigenschaften und privaten Slots (nur das Wesentliche). Wenn Sie tiefer graben möchten, können Sie dies hier tun

Die folgende Klasse demonstriert die beiden Arten von Slots. Jede ihrer Instanzen hat ein privates Feld und eine Eigenschaft

class MyClass {
  #instancePrivateField = 1;
  instanceProperty = 2;
  getInstanceValues() {
    return [
      this.#instancePrivateField,
      this.instanceProperty,
    ];
  }
}
const inst = new MyClass();
assert.deepEqual(
  inst.getInstanceValues(), [1, 2]
);

Wie erwartet, außerhalb von MyClass können wir nur die Eigenschaft sehen

assert.deepEqual(
  Reflect.ownKeys(inst),
  ['instanceProperty']
);

Als Nächstes sehen wir uns einige Details zu privaten Slots an.

29.2.5 Private Slots im Detail [ES2022] (fortgeschritten)

29.2.5.1 Private Slots können nicht in Unterklassen aufgerufen werden

Ein privater Slot kann wirklich nur im Körper seiner Klasse aufgerufen werden. Wir können ihn nicht einmal von einer Unterklasse aus aufrufen

class SuperClass {
  #superProp = 'superProp';
}
class SubClass extends SuperClass {
  getSuperProp() {
    return this.#superProp;
  }
}
// SyntaxError: Private field '#superProp'
// must be declared in an enclosing class

Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt. Wie man diese Einschränkung umgehen kann, wird in §29.5.4 „Simulation von geschützter und befreundeter Sichtbarkeit mittels WeakMaps“ erklärt.

29.2.5.2 Jeder private Slot hat einen eindeutigen Schlüssel (einen privaten Namen)

Private Slots haben eindeutige Schlüssel, die Symbolen ähneln. Betrachten Sie die folgende frühere Klasse

class MyClass {
  #instancePrivateField = 1;
  instanceProperty = 2;
  getInstanceValues() {
    return [
      this.#instancePrivateField,
      this.instanceProperty,
    ];
  }
}

Intern wird das private Feld von MyClass ungefähr so behandelt

let MyClass;
{ // Scope of the body of the class
  const instancePrivateFieldKey = Symbol();
  MyClass = class {
    // Very loose approximation of how this
    // works in the language specification
    __PrivateElements__ = new Map([
      [instancePrivateFieldKey, 1],
    ]);
    instanceProperty = 2;
    getInstanceValues() {
      return [
        this.__PrivateElements__.get(instancePrivateFieldKey),
        this.instanceProperty,
      ];
    }
  }
}

Der Wert von instancePrivateFieldKey wird als privater Name bezeichnet. Wir können private Namen in JavaScript nicht direkt verwenden, sondern nur indirekt über die festen Bezeichner von privaten Feldern, privaten Methoden und privaten Accessoren. Wo die festen Bezeichner öffentlicher Slots (wie getInstanceValues) als Zeichenkettenschlüssel interpretiert werden, beziehen sich die festen Bezeichner privater Slots (wie #instancePrivateField) auf private Namen (ähnlich wie Variablennamen auf Werte verweisen).

29.2.5.3 Der gleiche private Bezeichner verweist auf unterschiedliche private Namen in verschiedenen Klassen

Da die Bezeichner privater Slots nicht als Schlüssel verwendet werden, erzeugt die Verwendung desselben Bezeichners in verschiedenen Klassen unterschiedliche Slots (Zeile A und Zeile C)

class Color {
  #name; // (A)
  constructor(name) {
    this.#name = name; // (B)
  }
  static getName(obj) {
    return obj.#name;
  }
}
class Person {
  #name; // (C)
  constructor(name) {
    this.#name = name;
  }
}

assert.equal(
  Color.getName(new Color('green')), 'green'
);

// We can’t access the private slot #name of a Person in line B:
assert.throws(
  () => Color.getName(new Person('Jane')),
  {
    name: 'TypeError',
    message: 'Cannot read private member #name from'
      + ' an object whose class did not declare it',
  }
);
29.2.5.4 Die Namen privater Felder kollidieren niemals

Selbst wenn eine Unterklasse denselben Namen für ein privates Feld verwendet, kollidieren die beiden Namen niemals, da sie auf private Namen verweisen (die immer eindeutig sind). Im folgenden Beispiel kollidiert .#privateField in SuperClass nicht mit .#privateField in SubClass, obwohl beide Slots direkt in inst gespeichert werden

class SuperClass {
  #privateField = 'super';
  getSuperPrivateField() {
    return this.#privateField;
  }
}
class SubClass extends SuperClass {
  #privateField = 'sub';
  getSubPrivateField() {
    return this.#privateField;
  }
}
const inst = new SubClass();
assert.equal(
  inst.getSuperPrivateField(), 'super'
);
assert.equal(
  inst.getSubPrivateField(), 'sub'
);

Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.

29.2.5.5 Verwendung von in, um zu prüfen, ob ein Objekt einen bestimmten privaten Slot hat

Der in-Operator kann verwendet werden, um zu prüfen, ob ein privater Slot existiert (Zeile A)

class Color {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj; // (A)
  }
}

Sehen wir uns weitere Beispiele für die Anwendung von in auf private Slots an.

Private Methoden. Der folgende Code zeigt, dass private Methoden private Slots in Instanzen erstellen

class C1 {
  #priv() {}
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C1.check(new C1()), true);

Statische private Felder. Wir können in auch für ein statisches privates Feld verwenden

class C2 {
  static #priv = 1;
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false);

Statische private Methoden. Und wir können den Slot einer statischen privaten Methode prüfen

class C3 {
  static #priv() {}
  static check(obj) {
    return #priv in obj;
  }
}
assert.equal(C3.check(C3), true);

Verwendung desselben privaten Bezeichners in verschiedenen Klassen. Im folgenden Beispiel haben die beiden Klassen Color und Person beide einen Slot, dessen Bezeichner #name ist. Der in-Operator unterscheidet sie korrekt

class Color {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}
class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  static check(obj) {
    return #name in obj;
  }
}

// Detecting Color’s #name
assert.equal(
  Color.check(new Color()), true
);
assert.equal(
  Color.check(new Person()), false
);

// Detecting Person’s #name
assert.equal(
  Person.check(new Person()), true
);
assert.equal(
  Person.check(new Color()), false
);

29.2.6 Die Vor- und Nachteile von Klassen in JavaScript

Ich empfehle die Verwendung von Klassen aus folgenden Gründen

Das bedeutet nicht, dass Klassen perfekt sind

Dies war ein erster Blick auf Klassen. Wir werden bald weitere Funktionen untersuchen.

  Übung: Eine Klasse schreiben

exercises/classes/point_class_test.mjs

29.2.7 Tipps zur Verwendung von Klassen

29.3 Das Innere von Klassen

29.3.1 Eine Klasse sind eigentlich zwei verbundene Objekte

Unter der Haube wird eine Klasse zu zwei verbundenen Objekten. Lassen Sie uns die Klasse Person noch einmal betrachten, um zu sehen, wie das funktioniert

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}

Das erste von der Klasse erzeugte Objekt wird in Person gespeichert. Es hat vier Eigenschaften

assert.deepEqual(
  Reflect.ownKeys(Person),
  ['length', 'name', 'prototype', 'extractNames']
);

// The number of parameters of the constructor
assert.equal(
  Person.length, 1
);

// The name of the class
assert.equal(
  Person.name, 'Person'
);

Die beiden verbleibenden Eigenschaften sind

Das sind die Inhalte von Person.prototype

assert.deepEqual(
  Reflect.ownKeys(Person.prototype),
  ['constructor', 'describe']
);

Es gibt zwei Eigenschaften

29.3.2 Klassen richten die Prototypketten ihrer Instanzen ein

Das Objekt Person.prototype ist der Prototyp aller Instanzen

const jane = new Person('Jane');
assert.equal(
  Object.getPrototypeOf(jane), Person.prototype
);

const tarzan = new Person('Tarzan');
assert.equal(
  Object.getPrototypeOf(tarzan), Person.prototype
);

Das erklärt, wie die Instanzen ihre Methoden erhalten: Sie erben sie vom Objekt Person.prototype.

Abb. 13 visualisiert, wie alles verbunden ist.

Figure 13: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. The objects jane and tarzan are two such instances.

29.3.3 .__proto__ vs. .prototype

Es ist leicht, .__proto__ und .prototype zu verwechseln. Hoffentlich macht Abb. 13 klar, wie sie sich unterscheiden

29.3.4 Person.prototype.constructor (fortgeschritten)

Es gibt ein Detail in Abb. 13, das wir uns noch nicht angesehen haben: Person.prototype.constructor verweist zurück auf Person

> Person.prototype.constructor === Person
true

Diese Einrichtung besteht aus Gründen der Abwärtskompatibilität. Sie hat aber zwei zusätzliche Vorteile.

Erstens erbt jede Instanz einer Klasse die Eigenschaft .constructor. Daher können wir über eine Instanz „ähnliche“ Objekte erstellen

const jane = new Person('Jane');

const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
assert.equal(cheeta instanceof Person, true);

Zweitens können wir den Namen der Klasse erhalten, die eine gegebene Instanz erstellt hat

const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');

29.3.5 Dispatchete vs. direkte Methodenaufrufe (fortgeschritten)

In diesem Unterabschnitt lernen wir zwei verschiedene Möglichkeiten kennen, Methoden aufzurufen

Das Verständnis beider wird uns wichtige Einblicke in die Funktionsweise von Methoden geben.

Wir werden die zweite Methode später in diesem Kapitel benötigen: Sie wird es uns ermöglichen, nützliche Methoden von Object.prototype zu „leihen“.

29.3.5.1 Dispatchete Methodenaufrufe

Betrachten wir, wie Methodenaufrufe mit Klassen funktionieren. Wir greifen noch einmal auf jane von früher zurück

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
    return 'Person named '+this.#firstName;
  }
}
const jane = new Person('Jane');

Abb. 14 enthält ein Diagramm mit janes Prototypkette.

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

Normale Methodenaufrufe werden dispatched – der Methodenaufruf

jane.describe()

erfolgt in zwei Schritten

Diese Art der dynamischen Suche nach einer Methode und ihres Aufrufs wird als dynamischer Dispatch bezeichnet.

29.3.5.2 Direkte Methodenaufrufe

Wir können Methodenaufrufe auch direkt, ohne Dispatching, vornehmen

Person.prototype.describe.call(jane)

Diesmal zeigen wir direkt auf die Methode über Person.prototype.describe und suchen nicht in der Prototypkette danach. Wir geben auch this anders an – über .call().

  this zeigt immer auf die Instanz

Unabhängig davon, wo sich eine Methode in der Prototypkette einer Instanz befindet, zeigt this immer auf die Instanz (den Anfang der Prototypkette). Dies ermöglicht es .describe(), auf .#firstName im Beispiel zuzugreifen.

Wann sind direkte Methodenaufrufe nützlich? Immer dann, wenn wir eine Methode von anderswo „leihen“ wollen, die ein bestimmtes Objekt nicht hat – zum Beispiel

const obj = Object.create(null);

// `obj` is not an instance of Object and doesn’t inherit
// its prototype method .toString()
assert.throws(
  () => obj.toString(),
  /^TypeError: obj.toString is not a function$/
);
assert.equal(
  Object.prototype.toString.call(obj),
  '[object Object]'
);

29.3.6 Klassen entwickelten sich aus gewöhnlichen Funktionen (fortgeschritten)

Vor ECMAScript 6 hatte JavaScript keine Klassen. Stattdessen wurden gewöhnliche Funktionen als Konstruktorfunktionen verwendet

function StringBuilderConstr(initialString) {
  this.string = initialString;
}
StringBuilderConstr.prototype.add = function (str) {
  this.string += str;
  return this;
};

const sb = new StringBuilderConstr('¡');
sb.add('Hola').add('!');
assert.equal(
  sb.string, '¡Hola!'
);

Klassen bieten eine bessere Syntax für diesen Ansatz

class StringBuilderClass {
  constructor(initialString) {
    this.string = initialString;
  }
  add(str) {
    this.string += str;
    return this;
  }
}
const sb = new StringBuilderClass('¡');
sb.add('Hola').add('!');
assert.equal(
  sb.string, '¡Hola!'
);

Die Unterklassenbildung ist mit Konstruktorfunktionen besonders knifflig. Klassen bieten auch Vorteile, die über eine bequemere Syntax hinausgehen

Klassen sind so kompatibel mit Konstruktorfunktionen, dass sie diese sogar erweitern können

function SuperConstructor() {}
class SubClass extends SuperConstructor {}

assert.equal(
  new SubClass() instanceof SuperConstructor, true
);

extends und Unterklassenbildung werden später in diesem Kapitel erklärt.

29.3.6.1 Eine Klasse ist der Konstruktor

Das bringt uns zu einer interessanten Erkenntnis. Einerseits bezieht sich StringBuilderClass über StringBuilderClass.prototype.constructor auf seinen Konstruktor.

Andererseits ist die Klasse der Konstruktor (eine Funktion)

> StringBuilderClass.prototype.constructor === StringBuilderClass
true
> typeof StringBuilderClass
'function'

  Konstruktor (Funktionen) vs. Klassen

Aufgrund ihrer Ähnlichkeit verwende ich die Begriffe Konstruktor (Funktion) und Klasse austauschbar.

29.4 Prototyp-Mitglieder von Klassen

29.4.1 Öffentliche Prototyp-Methoden und Accessoren

Alle Mitglieder im Körper der folgenden Klassendeklaration erstellen Eigenschaften von PublicProtoClass.prototype.

class PublicProtoClass {
  constructor(args) {
    // (Do something with `args` here.)
  }
  publicProtoMethod() {
    return 'publicProtoMethod';
  }
  get publicProtoAccessor() {
    return 'publicProtoGetter';
  }
  set publicProtoAccessor(value) {
    assert.equal(value, 'publicProtoSetter');
  }
}

assert.deepEqual(
  Reflect.ownKeys(PublicProtoClass.prototype),
  ['constructor', 'publicProtoMethod', 'publicProtoAccessor']
);

const inst = new PublicProtoClass('arg1', 'arg2');
assert.equal(
  inst.publicProtoMethod(), 'publicProtoMethod'
);
assert.equal(
  inst.publicProtoAccessor, 'publicProtoGetter'
);
inst.publicProtoAccessor = 'publicProtoSetter';
29.4.1.1 Alle Arten von öffentlichen Prototyp-Methoden und Accessoren (fortgeschritten)
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');

class PublicProtoClass2 {
  // Identifier keys
  get accessor() {}
  set accessor(value) {}
  syncMethod() {}
  * syncGeneratorMethod() {}
  async asyncMethod() {}
  async * asyncGeneratorMethod() {}

  // Quoted keys
  get 'an accessor'() {}
  set 'an accessor'(value) {}
  'sync method'() {}
  * 'sync generator method'() {}
  async 'async method'() {}
  async * 'async generator method'() {}

  // Computed keys
  get [accessorKey]() {}
  set [accessorKey](value) {}
  [syncMethodKey]() {}
  * [syncGenMethodKey]() {}
  async [asyncMethodKey]() {}
  async * [asyncGenMethodKey]() {}
}

// Quoted and computed keys are accessed via square brackets:
const inst = new PublicProtoClass2();
inst['sync method']();
inst[syncMethodKey]();

Anführungszeichen- und berechnete Schlüssel können auch in Objektliteralen verwendet werden

Weitere Informationen zu Accessoren (definiert über Getter und/oder Setter), Generatoren, asynchronen Methoden und asynchronen Generator-Methoden

29.4.2 Private Methoden und Accessoren [ES2022]

Private Methoden (und Accessoren) sind eine interessante Mischung aus Prototyp-Mitgliedern und Instanz-Mitgliedern.

Einerseits werden private Methoden in Slots in Instanzen gespeichert (Zeile A)

class MyClass {
  #privateMethod() {}
  static check() {
    const inst = new MyClass();
    assert.equal(
      #privateMethod in inst, true // (A)
    );
    assert.equal(
      #privateMethod in MyClass.prototype, false
    );
    assert.equal(
      #privateMethod in MyClass, false
    );
  }
}
MyClass.check();

Warum werden sie nicht in .prototype-Objekten gespeichert? Private Slots werden nicht vererbt, nur Eigenschaften.

Andererseits werden private Methoden zwischen Instanzen geteilt – wie öffentliche Prototyp-Methoden

class MyClass {
  #privateMethod() {}
  static check() {
    const inst1 = new MyClass();
    const inst2 = new MyClass();
    assert.equal(
      inst1.#privateMethod,
      inst2.#privateMethod
    );
  }
}

Aufgrund dessen und weil ihre Syntax der von öffentlichen Prototyp-Methoden ähnelt, werden sie hier behandelt.

Der folgende Code demonstriert, wie private Methoden und Accessoren funktionieren

class PrivateMethodClass {
  #privateMethod() {
    return 'privateMethod';
  }
  get #privateAccessor() {
    return 'privateGetter';
  }
  set #privateAccessor(value) {
    assert.equal(value, 'privateSetter');
  }
  callPrivateMembers() {
    assert.equal(this.#privateMethod(), 'privateMethod');
    assert.equal(this.#privateAccessor, 'privateGetter');
    this.#privateAccessor = 'privateSetter';
  }
}
assert.deepEqual(
  Reflect.ownKeys(new PrivateMethodClass()), []
);
29.4.2.1 Alle Arten von privaten Methoden und Accessoren (fortgeschritten)

Bei privaten Slots sind die Schlüssel immer Bezeichner

class PrivateMethodClass2 {
  get #accessor() {}
  set #accessor(value) {}
  #syncMethod() {}
  * #syncGeneratorMethod() {}
  async #asyncMethod() {}
  async * #asyncGeneratorMethod() {}
}

Weitere Informationen zu Accessoren (definiert über Getter und/oder Setter), Generatoren, asynchronen Methoden und asynchronen Generator-Methoden

29.5 Instanz-Mitglieder von Klassen [ES2022]

29.5.1 Instanz-öffentliche Felder

Instanzen der folgenden Klasse haben zwei Instanz-Eigenschaften (erstellt in Zeile A und Zeile B)

class InstPublicClass {
  // Instance public field
  instancePublicField = 0; // (A)

  constructor(value) {
    // We don’t need to mention .property elsewhere!
    this.property = value; // (B)
  }
}

const inst = new InstPublicClass('constrArg');
assert.deepEqual(
  Reflect.ownKeys(inst),
  ['instancePublicField', 'property']
);
assert.equal(
  inst.instancePublicField, 0
);
assert.equal(
  inst.property, 'constrArg'
);

Wenn wir eine Instanzeigenschaft innerhalb des Konstruktors erstellen (Zeile B), müssen wir sie nicht woanders „deklarieren“. Wie wir bereits gesehen haben, ist das bei Instanz-privaten Feldern anders.

Beachten Sie, dass Instanz-Eigenschaften in JavaScript relativ häufig vorkommen; viel häufiger als z. B. in Java, wo die meisten Instanzzustände privat sind.

29.5.1.1 Instanz-öffentliche Felder mit Anführungszeichen- und berechneten Schlüsseln (fortgeschritten)
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
  'quoted field key' = 1;
  [computedFieldKey] = 2;
}
const inst = new InstPublicClass2();
assert.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2);
29.5.1.2 Was ist der Wert von this in Instanz-öffentlichen Feldern? (fortgeschritten)

Im Initialisierer eines Instanz-öffentlichen Feldes bezieht sich this auf die neu erstellte Instanz

class MyClass {
  instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
  inst.instancePublicField, inst
);
29.5.1.3 Wann werden Instanz-öffentliche Felder ausgeführt? (fortgeschritten)

Die Ausführung von Instanz-öffentlichen Feldern folgt ungefähr diesen beiden Regeln

Das folgende Beispiel demonstriert diese Regeln

class SuperClass {
  superProp = console.log('superProp');
  constructor() {
    console.log('super-constructor');
  }
}
class SubClass extends SuperClass {
  subProp = console.log('subProp');
  constructor() {
    console.log('BEFORE super()');
    super();
    console.log('AFTER super()');
  }
}
new SubClass();

// Output:
// 'BEFORE super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'AFTER super()'

extends und Unterklassenbildung werden später in diesem Kapitel erklärt.

29.5.2 Instanz-private Felder

Die folgende Klasse enthält zwei Instanz-private Felder (Zeile A und Zeile B)

class InstPrivateClass {
  #privateField1 = 'private field 1'; // (A)
  #privateField2; // (B) required!
  constructor(value) {
    this.#privateField2 = value; // (C)
  }
  /**
   * Private fields are not accessible outside the class body.
   */
  checkPrivateValues() {
    assert.equal(
      this.#privateField1, 'private field 1'
    );
    assert.equal(
      this.#privateField2, 'constructor argument'
    );
  }
}

const inst = new InstPrivateClass('constructor argument');
  inst.checkPrivateValues();

// No instance properties were created
assert.deepEqual(
  Reflect.ownKeys(inst),
  []
);

Beachten Sie, dass wir .#privateField2 in Zeile C nur verwenden können, wenn wir es im Klassenkörper deklarieren.

29.5.3 Private Instanzdaten vor ES2022 (fortgeschritten)

In diesem Abschnitt sehen wir uns zwei Techniken an, um Instanzdaten privat zu halten. Da sie nicht auf Klassen basieren, können wir sie auch für Objekte verwenden, die auf andere Weise erstellt wurden – z. B. über Objektliterale.

29.5.3.1 Vor ES6: private Mitglieder durch Namenskonventionen

Die erste Technik macht eine Eigenschaft privat, indem sie ihr einen Unterstrich voranstellt. Dies schützt die Eigenschaft in keiner Weise; es signalisiert lediglich nach außen: „Sie müssen diese Eigenschaft 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 wir keinen Schutz und private Namen können kollidieren. Auf der positiven Seite ist sie einfach zu verwenden.

Private Methoden funktionieren ähnlich: Es sind normale Methoden, deren Namen mit Unterstrichen beginnen.

29.5.3.2 ES6 und später: private Instanzdaten über WeakMaps

Wir können private Instanzdaten auch über WeakMaps verwalten

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()),
  []);

Wie genau das funktioniert, wird im Kapitel über WeakMaps erklärt.

Diese Technik bietet uns erheblichen Schutz vor externem Zugriff und es kann keine Namenskollisionen geben. Aber sie ist auch komplizierter zu verwenden.

Wir steuern die Sichtbarkeit der Pseudo-Eigenschaft _superProp, indem wir steuern, wer Zugriff darauf hat – zum Beispiel: Wenn die Variable innerhalb eines Moduls existiert und nicht exportiert wird, können alle innerhalb des Moduls und niemand außerhalb des Moduls darauf zugreifen. Mit anderen Worten: Der Geltungsbereich der Privatsphäre ist in diesem Fall nicht die Klasse, sondern das Modul. Wir könnten den Bereich jedoch einschränken

let Countdown;
{ // class scope
  const _counter = new WeakMap();
  const _action = new WeakMap();

  Countdown = class {
    // ···
  }
}

Diese Technik unterstützt private Methoden nicht wirklich. Aber Modul-lokale Funktionen, die Zugriff auf _superProp haben, sind das Nächstbeste

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    privateDec(this);
  }
}

function privateDec(_this) { // (A)
  let counter = _counter.get(_this);
  counter--;
  _counter.set(_this, counter);
  if (counter === 0) {
    _action.get(_this)();
  }
}

Beachten Sie, dass this zum expliziten Funktionsparameter _this wird (Zeile A).

29.5.4 Simulation von geschützter und befreundeter Sichtbarkeit mittels WeakMaps (fortgeschritten)

Wie bereits erörtert, sind Instanz-private Felder nur innerhalb ihrer Klassen sichtbar und nicht einmal in Unterklassen. Daher gibt es keine eingebaute Möglichkeit,

Im vorherigen Unterabschnitt haben wir „Modulsichtbarkeit“ simuliert (jeder innerhalb eines Moduls hat Zugriff auf Instanzdaten) über WeakMaps. Daher

Das nächste Beispiel demonstriert geschützte Sichtbarkeit

const _superProp = new WeakMap();
class SuperClass {
  constructor() {
    _superProp.set(this, 'superProp');
  }
}
class SubClass extends SuperClass {
  getSuperProp() {
    return _superProp.get(this);
  }
}
assert.equal(
  new SubClass().getSuperProp(),
  'superProp'
);

Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.

29.6 Statische Mitglieder von Klassen

29.6.1 Statische öffentliche Methoden und Accessoren

Alle Mitglieder im Körper der folgenden Klassendeklaration erstellen sogenannte statische Eigenschaften – Eigenschaften von StaticClass selbst.

class StaticPublicMethodsClass {
  static staticMethod() {
    return 'staticMethod';
  }
  static get staticAccessor() {
    return 'staticGetter';
  }
  static set staticAccessor(value) {
    assert.equal(value, 'staticSetter');
  }
}
assert.equal(
  StaticPublicMethodsClass.staticMethod(), 'staticMethod'
);
assert.equal(
  StaticPublicMethodsClass.staticAccessor, 'staticGetter'
);
StaticPublicMethodsClass.staticAccessor = 'staticSetter';
29.6.1.1 Alle Arten von statischen öffentlichen Methoden und Accessoren (fortgeschritten)
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');

class StaticPublicMethodsClass2 {
  // Identifier keys
  static get accessor() {}
  static set accessor(value) {}
  static syncMethod() {}
  static * syncGeneratorMethod() {}
  static async asyncMethod() {}
  static async * asyncGeneratorMethod() {}

  // Quoted keys
  static get 'an accessor'() {}
  static set 'an accessor'(value) {}
  static 'sync method'() {}
  static * 'sync generator method'() {}
  static async 'async method'() {}
  static async * 'async generator method'() {}

  // Computed keys
  static get [accessorKey]() {}
  static set [accessorKey](value) {}
  static [syncMethodKey]() {}
  static * [syncGenMethodKey]() {}
  static async [asyncMethodKey]() {}
  static async * [asyncGenMethodKey]() {}
}

// Quoted and computed keys are accessed via square brackets:
StaticPublicMethodsClass2['sync method']();
StaticPublicMethodsClass2[syncMethodKey]();

Anführungszeichen- und berechnete Schlüssel können auch in Objektliteralen verwendet werden

Weitere Informationen zu Accessoren (definiert über Getter und/oder Setter), Generatoren, asynchronen Methoden und asynchronen Generator-Methoden

29.6.2 Statische öffentliche Felder [ES2022]

Der folgende Code demonstriert statische öffentliche Felder. StaticPublicFieldClass hat drei davon

const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
  static identifierFieldKey = 1;
  static 'quoted field key' = 2;
  static [computedFieldKey] = 3;
}

assert.deepEqual(
  Reflect.ownKeys(StaticPublicFieldClass),
  [
    'length', // number of constructor parameters
    'name', // 'StaticPublicFieldClass'
    'prototype',
    'identifierFieldKey',
    'quoted field key',
    computedFieldKey,
  ],
);

assert.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3);

29.6.3 Statische private Methoden, Accessoren und Felder [ES2022]

Die folgende Klasse hat zwei statische private Slots (Zeile A und Zeile B)

class StaticPrivateClass {
  // Declare and initialize
  static #staticPrivateField = 'hello'; // (A)
  static #twice() { // (B)
    const str = StaticPrivateClass.#staticPrivateField;
    return str + ' ' + str;
  }
  static getResultOfTwice() {
    return StaticPrivateClass.#twice();
  }
}

assert.deepEqual(
  Reflect.ownKeys(StaticPrivateClass),
  [
    'length', // number of constructor parameters
    'name', // 'StaticPublicFieldClass'
    'prototype',
    'getResultOfTwice',
  ],
);

assert.equal(
  StaticPrivateClass.getResultOfTwice(),
  'hello hello'
);

Dies ist eine vollständige Liste aller Arten von statischen privaten Slots

class MyClass {
  static #staticPrivateMethod() {}
  static * #staticPrivateGeneratorMethod() {}

  static async #staticPrivateAsyncMethod() {}
  static async * #staticPrivateAsyncGeneratorMethod() {}
  
  static get #staticPrivateAccessor() {}
  static set #staticPrivateAccessor(value) {}
}

29.6.4 Statische Initialisierungsblöcke in Klassen [ES2022]

Um Instanzdaten über Klassen einzurichten, haben wir zwei Konstrukte

Für statische Daten haben wir

Der folgende Code demonstriert statische Blöcke (Zeile A)

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static { // (A)
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

Wir könnten den Code im statischen Block auch nach der Klasse (auf oberster Ebene) ausführen. Die Verwendung eines statischen Blocks hat jedoch zwei Vorteile

29.6.4.1 Regeln für statische Initialisierungsblöcke

Die Regeln für die Funktionsweise statischer Initialisierungsblöcke sind relativ einfach

Der folgende Code demonstriert diese Regeln

class SuperClass {
  static superField1 = console.log('superField1');
  static {
    assert.equal(this, SuperClass);
    console.log('static block 1 SuperClass');
  }
  static superField2 = console.log('superField2');
  static {
    console.log('static block 2 SuperClass');
  }
}

class SubClass extends SuperClass {
  static subField1 = console.log('subField1');
  static {
    assert.equal(this, SubClass);
    console.log('static block 1 SubClass');
  }
  static subField2 = console.log('subField2');
  static {
    console.log('static block 2 SubClass');
  }
}

// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'

Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.

29.6.5 Fallstrick: Verwenden von this zum Zugriff auf statische private Felder

In statischen öffentlichen Mitgliedern können wir statische öffentliche Slots über this aufrufen. Leider sollten wir ihn nicht zum Aufrufen statischer privater Slots verwenden.

29.6.5.1 this und statische öffentliche Felder

Betrachten Sie den folgenden Code

class SuperClass {
  static publicData = 1;
  
  static getPublicViaThis() {
    return this.publicData;
  }
}
class SubClass extends SuperClass {
}

Unterklassenbildung mittels extends wird später in diesem Kapitel erklärt.

Statische öffentliche Felder sind Eigenschaften. Wenn wir den Methodenaufruf machen

assert.equal(SuperClass.getPublicViaThis(), 1);

dann zeigt this auf SuperClass und alles funktioniert wie erwartet. Wir können .getPublicViaThis() auch über die Unterklasse aufrufen

assert.equal(SubClass.getPublicViaThis(), 1);

SubClass erbt .getPublicViaThis() von seinem Prototyp SuperClass. this zeigt auf SubClass und die Dinge funktionieren weiterhin, weil SubClass auch die Eigenschaft .publicData erbt.

Nebenbei bemerkt: Wenn wir in getPublicViaThis() this.publicData zugewiesen hätten und es über SubClass.getPublicViaThis() aufgerufen hätten, würden wir eine neue eigene Eigenschaft von SubClass erstellen, die die von SuperClass geerbte Eigenschaft (nicht-destruktiv) überschreibt.

29.6.5.2 this und statische private Felder

Betrachten Sie den folgenden Code

class SuperClass {
  static #privateData = 2;
  static getPrivateDataViaThis() {
    return this.#privateData;
  }
  static getPrivateDataViaClassName() {
    return SuperClass.#privateData;
  }
}
class SubClass extends SuperClass {
}

Der Aufruf von .getPrivateDataViaThis() über SuperClass funktioniert, da this auf SuperClass zeigt

assert.equal(SuperClass.getPrivateDataViaThis(), 2);

Das Aufrufen von .getPrivateDataViaThis() über SubClass funktioniert jedoch nicht, da this nun auf SubClass verweist und SubClass kein statisches privates Feld .#privateData hat (private Slots in Prototypketten werden nicht vererbt).

assert.throws(
  () => SubClass.getPrivateDataViaThis(),
  {
    name: 'TypeError',
    message: 'Cannot read private member #privateData from'
      + ' an object whose class did not declare it',
  }
);

Der Workaround besteht darin, .#privateData direkt über SuperClass anzusprechen.

assert.equal(SubClass.getPrivateDataViaClassName(), 2);

Bei statischen privaten Methoden stehen wir vor demselben Problem.

29.6.6 Alle Member (statisch, Prototyp, Instanz) können auf alle privaten Member zugreifen

Jeder Member innerhalb einer Klasse kann auf alle anderen Member innerhalb dieser Klasse zugreifen – sowohl öffentliche als auch private.

class DemoClass {
  static #staticPrivateField = 1;
  #instPrivField = 2;

  static staticMethod(inst) {
    // A static method can access static private fields
    // and instance private fields
    assert.equal(DemoClass.#staticPrivateField, 1);
    assert.equal(inst.#instPrivField, 2);
  }

  protoMethod() {
    // A prototype method can access instance private fields
    // and static private fields
    assert.equal(this.#instPrivField, 2);
    assert.equal(DemoClass.#staticPrivateField, 1);
  }
}

Im Gegensatz dazu kann niemand außerhalb auf die privaten Member zugreifen.

// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
  () => eval('DemoClass.#staticPrivateField'),
  {
    name: 'SyntaxError',
    message: "Private field '#staticPrivateField' must"
      + " be declared in an enclosing class",
  }
);
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
  () => eval('new DemoClass().#instPrivField'),
  {
    name: 'SyntaxError',
    message: "Private field '#instPrivField' must"
      + " be declared in an enclosing class",
  }
);

29.6.7 Statische private Methoden und Daten vor ES2022

Der folgende Code funktioniert nur in ES2022 – aufgrund jeder Zeile, die ein Hash-Symbol (#) enthält.

class StaticClass {
  static #secret = 'Rumpelstiltskin';
  static #getSecretInParens() {
    return `(${StaticClass.#secret})`;
  }
  static callStaticPrivateMethod() {
    return StaticClass.#getSecretInParens();
  }
}

Da private Slots nur einmal pro Klasse existieren, können wir #secret und #getSecretInParens in den Scope verschieben, der die Klasse umgibt, und ein Modul verwenden, um sie von der Außenwelt des Moduls zu verbergen.

const secret = 'Rumpelstiltskin';
function getSecretInParens() {
  return `(${secret})`;
}

// Only the class is accessible outside the module
export class StaticClass {
  static callStaticPrivateMethod() {
    return getSecretInParens();
  }
}

29.6.8 Statische Factory-Methoden

Manchmal gibt es mehrere Möglichkeiten, wie eine Klasse instanziiert werden kann. Dann können wir statische Factory-Methoden implementieren, wie z. B. Point.fromPolar().

class Point {
  static fromPolar(radius, angle) {
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    return new Point(x, y);
  }
  constructor(x=0, y=0) {
    this.x = x;
    this.y = y;
  }
}

assert.deepEqual(
  Point.fromPolar(13, 0.39479111969976155),
  new Point(12, 5)
);

Ich mag, wie deskriptiv statische Factory-Methoden sind: fromPolar beschreibt, wie eine Instanz erstellt wird. Die Standardbibliothek von JavaScript hat auch solche Factory-Methoden – zum Beispiel

Ich bevorzuge es, entweder keine statischen Factory-Methoden zu haben oder nur statische Factory-Methoden. Dinge, die im letzteren Fall zu beachten sind

Im folgenden Code verwenden wir ein geheimes Token (Zeile A), um den Aufruf des Konstruktors von außerhalb des aktuellen Moduls zu verhindern.

// Only accessible inside the current module
const secretToken = Symbol('secretToken'); // (A)

export class Point {
  static create(x=0, y=0) {
    return new Point(secretToken, x, y);
  }
  static fromPolar(radius, angle) {
    const x = radius * Math.cos(angle);
    const y = radius * Math.sin(angle);
    return new Point(secretToken, x, y);
  }
  constructor(token, x, y) {
    if (token !== secretToken) {
      throw new TypeError('Must use static factory method');
    }
    this.x = x;
    this.y = y;
  }
}
Point.create(3, 4); // OK
assert.throws(
  () => new Point(3, 4),
  TypeError
);

29.7 Unterklassenbildung

Klassen können auch bestehende Klassen erweitern. Zum Beispiel erweitert die folgende Klasse Employee die Klasse Person.

class Person {
  #firstName;
  constructor(firstName) {
    this.#firstName = firstName;
  }
  describe() {
    return `Person named ${this.#firstName}`;
  }
  static extractNames(persons) {
    return persons.map(person => person.#firstName);
  }
}

class Employee extends Person {
  constructor(firstName, title) {
    super(firstName);
    this.title = title;
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.title,
  'CTO'
);
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)'
);

Terminologie im Zusammenhang mit dem Erweitern

Innerhalb des .constructor() einer abgeleiteten Klasse müssen wir den Super-Konstruktor über super() aufrufen, bevor wir auf this zugreifen können. Warum ist das so?

Betrachten wir eine Kette von Klassen

Wenn wir new C() aufrufen, ruft der Konstruktor von C rekursiv den Konstruktor von B auf, der wiederum rekursiv den Konstruktor von A aufruft. Instanzen werden immer in Basisklassen erstellt, bevor die Konstruktoren von Unterklassen ihre Slots hinzufügen. Daher existiert die Instanz noch nicht, bevor wir super() aufrufen, und wir können noch nicht über this darauf zugreifen.

Beachten Sie, dass statische öffentliche Slots vererbt werden. Zum Beispiel erbt Employee die statische Methode .extractNames().

> 'extractNames' in Employee
true

  Übung: Unterklassenbildung

exercises/classes/color_point_class_test.mjs

29.7.1 Das Innenleben der Unterklassenbildung (fortgeschritten)

Figure 15: 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. 15). Eine Schlüssel Erkenntnis zum Verständnis, wie diese Objekte miteinander verbunden sind, ist, dass es zwei Prototypketten gibt:

29.7.1.1 Die Instanz-Prototypkette (rechte Spalte)

Die Instanz-Prototypkette beginnt mit jane und geht weiter über Employee.prototype und Person.prototype. Prinzipiell endet die Prototypkette an dieser Stelle, aber wir erhalten noch ein weiteres Objekt: Object.prototype. Dieser Prototyp stellt Dienste für praktisch alle Objekte bereit, weshalb er hier ebenfalls enthalten ist.

> Object.getPrototypeOf(Person.prototype) === Object.prototype
true
29.7.1.2 Die Klassen-Prototypkette (linke Spalte)

In der Klassen-Prototypkette kommt zuerst Employee, dann Person. Danach geht die Kette weiter mit Function.prototype, die nur da ist, weil Person eine Funktion ist und Funktionen die Dienste von Function.prototype benötigen.

> Object.getPrototypeOf(Person) === Function.prototype
true

29.7.2 instanceof und Unterklassenbildung (fortgeschritten)

Wir haben noch nicht gelernt, wie instanceof wirklich funktioniert. Wie ermittelt instanceof, ob ein Wert x eine Instanz einer Klasse C ist (es kann eine direkte Instanz von C oder eine direkte Instanz einer Unterklasse von C sein)? Es prüft, ob C.prototype in der Prototypkette von x vorhanden ist. Das heißt, die folgenden beiden Ausdrücke sind äquivalent:

x instanceof C
C.prototype.isPrototypeOf(x)

Wenn wir zu Abb. 15 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

Beachten Sie, dass instanceof immer false zurückgibt, wenn seine linke Seite ein primitiver Wert ist.

> 'abc' instanceof String
false
> 123 instanceof Number
false

29.7.3 Nicht alle Objekte sind Instanzen von Object (fortgeschritten)

Ein Objekt (ein nicht-primitiver Wert) ist nur dann eine Instanz von Object, wenn Object.prototype in seiner Prototypkette vorhanden ist (siehe vorheriger Unterabschnitt im Link). Praktisch alle Objekte sind Instanzen von Object – zum Beispiel:

assert.equal(
  {a: 1} instanceof Object, true
);
assert.equal(
  ['a'] instanceof Object, true
);
assert.equal(
  /abc/g instanceof Object, true
);
assert.equal(
  new Map() instanceof Object, true
);

class C {}
assert.equal(
  new C() instanceof Object, true
);

Im nächsten Beispiel sind obj1 und obj2 beide Objekte (Zeile A und Zeile C), aber sie sind keine Instanzen von Object (Zeile B und Zeile D): Object.prototype ist nicht in ihren Prototypketten, da sie keine Prototypen haben.

const obj1 = {__proto__: null};
assert.equal(
  typeof obj1, 'object' // (A)
);
assert.equal(
  obj1 instanceof Object, false // (B)
);

const obj2 = Object.create(null);
assert.equal(
  typeof obj2, 'object' // (C)
);
assert.equal(
  obj2 instanceof Object, false // (D)
);

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

> typeof Object.prototype
'object'
> Object.getPrototypeOf(Object.prototype)
null
> Object.prototype instanceof Object
false

29.7.4 Prototypketten von integrierten Objekten (fortgeschritten)

Als Nächstes werden wir unser Wissen über Unterklassenbildung nutzen, um die Prototypketten einiger integrierter 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.7.4.1 Die Prototypkette von {}

Beginnen wir mit der Untersuchung von einfachen Objekten.

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

Abb. 16 zeigt ein Diagramm für diese Prototypkette. Wir können sehen, dass {} wirklich eine Instanz von Object ist – Object.prototype befindet sich in seiner Prototypkette.

29.7.4.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 17: The prototype chain of an Array has these members: the Array instance, Array.prototype, Object.prototype, null.

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

29.7.4.3 Die Prototypkette von function () {}

Schließlich zeigt die Prototypkette einer gewöhnlichen Funktion, dass alle Funktionen Objekte sind.

> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true
29.7.4.4 Die Prototypketten von integrierten Klassen

Der Prototyp einer Basisklasse ist Function.prototype, was bedeutet, dass sie eine Funktion ist (eine Instanz von Function).

class A {}
assert.equal(
  Object.getPrototypeOf(A),
  Function.prototype
);

assert.equal(
  Object.getPrototypeOf(class {}),
  Function.prototype
);

Der Prototyp einer abgeleiteten Klasse ist ihre Superklasse.

class B extends A {}
assert.equal(
  Object.getPrototypeOf(B),
  A
);

assert.equal(
  Object.getPrototypeOf(class extends Object {}),
  Object
);

Interessanterweise sind Object, Array und Function alles Basisklassen.

> Object.getPrototypeOf(Object) === Function.prototype
true
> Object.getPrototypeOf(Array) === Function.prototype
true
> Object.getPrototypeOf(Function) === Function.prototype
true

Wie wir jedoch gesehen haben, haben selbst die Instanzen von Basisklassen Object.prototype in ihren Prototypketten, da es Dienste bereitstellt, die alle Objekte benötigen.

  Warum sind Array und Function Basisklassen?

Basisklassen sind dort, wo Instanzen tatsächlich erstellt werden. Sowohl Array als auch Function müssen ihre eigenen Instanzen erstellen, da sie sogenannte „interne Slots“ haben, die nachträglich nicht zu von Object erstellten Instanzen hinzugefügt werden können.

29.7.5 Mixin-Klassen (fortgeschritten)

Das Klassensystem von JavaScript unterstützt nur einfache Vererbung. Das heißt, jede Klasse kann höchstens eine Superklasse haben. Ein Weg, diese Einschränkung zu umgehen, ist die Verwendung einer Technik namens Mixin-Klassen (kurz: Mixins).

Die Idee ist wie folgt: Nehmen wir an, wir möchten, dass eine Klasse C von zwei Superklassen S1 und S2 erbt. Das wäre mehrfache Vererbung, die JavaScript nicht unterstützt.

Unser Workaround besteht darin, S1 und S2 zu Mixins, Fabriken für Unterklassen, zu machen.

const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };

Jede dieser beiden Funktionen gibt eine Klasse zurück, die eine gegebene Superklasse Sup erweitert. Wir erstellen die Klasse C wie folgt:

class C extends S2(S1(Object)) {
  /*···*/
}

Wir haben nun eine Klasse C, die die von S2() zurückgegebene Klasse erweitert, die die von S1() zurückgegebene Klasse erweitert, die Object erweitert.

29.7.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 Named = (Sup) => class extends Sup {
  name = '(Unnamed)';
  toString() {
    const className = this.constructor.name;
    return `${className} named ${this.name}`;
  }
};

Wir verwenden dieses Mixin, um eine Klasse City zu implementieren, die einen Namen hat.

class City extends Named(Object) {
  constructor(name) {
    super();
    this.name = name;
  }
}

Der folgende Code bestätigt, dass das Mixin funktioniert.

const paris = new City('Paris');
assert.equal(
  paris.name, 'Paris'
);
assert.equal(
  paris.toString(), 'City named Paris'
);
29.7.5.2 Die Vorteile von Mixins

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

29.8 Die Methoden und Accessoren von Object.prototype (fortgeschritten)

Wie wir in §29.7.3 „Nicht alle Objekte sind Instanzen von Object“ gesehen haben, sind fast alle Objekte Instanzen von Object. Diese Klasse stellt mehrere nützliche Methoden und einen Accessor für ihre Instanzen bereit.

Bevor wir uns jedes dieser Features genauer ansehen, lernen wir einen wichtigen Fallstrick kennen (und wie man ihn umgeht): Wir können die Features von Object.prototype nicht mit allen Objekten verwenden.

29.8.1 Sichere Verwendung von Object.prototype-Methoden

Das Aufrufen einer der Methoden von Object.prototype auf einem beliebigen Objekt funktioniert nicht immer. Um zu veranschaulichen, warum, verwenden wir die Methode Object.prototype.hasOwnProperty, die true zurückgibt, wenn ein Objekt eine eigene Eigenschaft mit einem gegebenen Schlüssel hat.

> {ownProp: true}.hasOwnProperty('ownProp')
true
> {ownProp: true}.hasOwnProperty('abc')
false

Das Aufrufen von .hasOwnProperty() auf einem beliebigen Objekt kann auf zwei Arten fehlschlagen. Einerseits ist diese Methode nicht verfügbar, wenn ein Objekt keine Instanz von Object ist (siehe §29.7.3 „Nicht alle Objekte sind Instanzen von Object“).

const obj = Object.create(null);
assert.equal(obj instanceof Object, false);
assert.throws(
  () => obj.hasOwnProperty('prop'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);

Andererseits können wir .hasOwnProperty() nicht verwenden, wenn ein Objekt sie mit einer eigenen Eigenschaft überschreibt (Zeile A).

const obj = {
  hasOwnProperty: 'yes' // (A)
};
assert.throws(
  () => obj.hasOwnProperty('prop'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);

Es gibt jedoch eine sichere Möglichkeit, .hasOwnProperty() zu verwenden.

function hasOwnProp(obj, propName) {
  return Object.prototype.hasOwnProperty.call(obj, propName); // (A)
}
assert.equal(
  hasOwnProp(Object.create(null), 'prop'), false
);
assert.equal(
  hasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
);
assert.equal(
  hasOwnProp({hasOwnProperty: 'yes'}, 'hasOwnProperty'), true
);

Der Methodenaufruf in Zeile A wird in §29.3.5 „Dispatched vs. direkte Methodenaufrufe“ erklärt.

Wir können auch .bind() verwenden, um hasOwnProp() zu implementieren.

const hasOwnProp = Object.prototype.hasOwnProperty.call
  .bind(Object.prototype.hasOwnProperty);

Wie funktioniert das? Wenn wir .call() wie in Zeile A im vorherigen Beispiel aufrufen, tut es genau das, was hasOwnProp() tun soll, einschließlich der Vermeidung von Fallstricken. Wenn wir es jedoch als Funktionsaufruf verwenden möchten, können wir es nicht einfach extrahieren, wir müssen auch sicherstellen, dass sein this immer den richtigen Wert hat. Das ist es, was .bind() tut.

  Ist es nie in Ordnung, Object.prototype-Methoden über dynamische Dispatch zu verwenden?

In einigen Fällen können wir faul sein und Object.prototype-Methoden wie normale Methoden aufrufen (ohne .call() oder .bind()): Wenn wir die Receiver kennen und es sich um Objekte mit festem Layout handelt.

Wenn wir andererseits ihre Receiver nicht kennen und/oder es sich um Dictionary-Objekte handelt, müssen wir Vorsichtsmaßnahmen treffen.

29.8.2 Object.prototype.toString()

Durch das Überschreiben von .toString() (in einer Unterklasse oder einer Instanz) können wir konfigurieren, wie Objekte in Strings umgewandelt werden.

> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'

Für die Umwandlung von Objekten in Strings ist es besser, String() zu verwenden, da dies auch mit undefined und null funktioniert.

> undefined.toString()
TypeError: Cannot read properties of undefined (reading 'toString')
> null.toString()
TypeError: Cannot read properties of null (reading 'toString')
> String(undefined)
'undefined'
> String(null)
'null'

29.8.3 Object.prototype.toLocaleString()

.toLocaleString() ist eine Version von .toString(), die über eine Locale und oft zusätzliche Optionen konfiguriert werden kann. Jede Klasse oder Instanz kann diese Methode implementieren. In der Standardbibliothek tun dies die folgenden Klassen:

Als Beispiel wird hier gezeigt, wie Zahlen mit Dezimalbrüchen je nach Locale unterschiedlich in Strings umgewandelt werden ('fr' ist Französisch, 'en' ist Englisch).

> 123.45.toLocaleString('fr')
'123,45'
> 123.45.toLocaleString('en')
'123.45'

29.8.4 Object.prototype.valueOf()

Durch das Überschreiben von .valueOf() (in einer Unterklasse oder einer Instanz) können wir konfigurieren, wie Objekte in nicht-String-Werte (oft Zahlen) umgewandelt werden.

> Number({valueOf() { return 123 }})
123
> Number({})
NaN

29.8.5 Object.prototype.isPrototypeOf()

proto.isPrototypeOf(obj) gibt true zurück, wenn proto in der Prototypkette von obj vorhanden ist, und andernfalls false.

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

So verwenden Sie diese Methode sicher (Details siehe §29.8.1 „Sichere Verwendung von Object.prototype-Methoden“).

const obj = {
  // Overrides Object.prototype.isPrototypeOf
  isPrototypeOf: true,
};
// Doesn’t work in this case:
assert.throws(
  () => obj.isPrototypeOf(Object.prototype),
  {
    name: 'TypeError',
    message: 'obj.isPrototypeOf is not a function',
  }
);
// Safe way of using .isPrototypeOf():
assert.equal(
  Object.prototype.isPrototypeOf.call(obj, Object.prototype), false
);

29.8.6 Object.prototype.propertyIsEnumerable()

obj.propertyIsEnumerable(propKey) gibt true zurück, wenn obj eine eigene aufzählbare Eigenschaft mit dem Schlüssel propKey hat, und andernfalls false.

const proto = {
  enumerableProtoProp: true,
};
const obj = {
  __proto__: proto,
  enumerableObjProp: true,
  nonEnumObjProp: true,
};
Object.defineProperty(
  obj, 'nonEnumObjProp',
  {
    enumerable: false,
  }
);

assert.equal(
  obj.propertyIsEnumerable('enumerableProtoProp'),
  false // not an own property
);
assert.equal(
  obj.propertyIsEnumerable('enumerableObjProp'),
  true
);
assert.equal(
  obj.propertyIsEnumerable('nonEnumObjProp'),
  false // not enumerable
);
assert.equal(
  obj.propertyIsEnumerable('unknownProp'),
  false // not a property
);

So verwenden Sie diese Methode sicher (Details siehe §29.8.1 „Sichere Verwendung von Object.prototype-Methoden“).

const obj = {
  // Overrides Object.prototype.propertyIsEnumerable
  propertyIsEnumerable: true,
  enumerableProp: 'yes',
};
// Doesn’t work in this case:
assert.throws(
  () => obj.propertyIsEnumerable('enumerableProp'),
  {
    name: 'TypeError',
    message: 'obj.propertyIsEnumerable is not a function',
  }
);
// Safe way of using .propertyIsEnumerable():
assert.equal(
  Object.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
  true
);

Eine weitere sichere Alternative sind Property-Deskriptoren.

assert.deepEqual(
  Object.getOwnPropertyDescriptor(obj, 'enumerableProp'),
  {
    value: 'yes',
    writable: true,
    enumerable: true,
    configurable: true,
  }
);

29.8.7 Object.prototype.__proto__ (Accessor)

Die Eigenschaft __proto__ existiert in zwei Versionen:

Ich empfehle, die erstere Funktion zu vermeiden.

Im Gegensatz dazu funktioniert __proto__ in Objekt-Literalen immer und ist nicht veraltet.

Lesen Sie weiter, wenn Sie daran interessiert sind, wie der Accessor __proto__ funktioniert.

__proto__ ist ein Accessor von Object.prototype, der von allen Instanzen von Object geerbt wird. Die Implementierung über eine Klasse würde so aussehen:

class Object {
  get __proto__() {
    return Object.getPrototypeOf(this);
  }
  set __proto__(other) {
    Object.setPrototypeOf(this, other);
  }
  // ···
}

Da __proto__ von Object.prototype geerbt wird, können wir dieses Feature entfernen, indem wir ein Objekt erstellen, das Object.prototype nicht in seiner Prototypkette hat (siehe §29.7.3 „Nicht alle Objekte sind Instanzen von Object“).

> '__proto__' in {}
true
> '__proto__' in Object.create(null)
false

29.8.8 Object.prototype.hasOwnProperty()

  Bessere Alternative zu .hasOwnProperty(): Object.hasOwn() [ES2022]

Siehe §28.9.4 „Object.hasOwn(): Ist eine gegebene Eigenschaft eine eigene (nicht geerbte)? [ES2022]“.

obj.hasOwnProperty(propKey) gibt true zurück, wenn obj eine eigene (nicht geerbte) Eigenschaft mit dem Schlüssel propKey hat, und andernfalls false.

const obj = { ownProp: true };
assert.equal(
  obj.hasOwnProperty('ownProp'), true // own
);
assert.equal(
  'toString' in obj, true // inherited
);
assert.equal(
  obj.hasOwnProperty('toString'), false
);

So verwenden Sie diese Methode sicher (Details siehe §29.8.1 „Sichere Verwendung von Object.prototype-Methoden“).

const obj = {
  // Overrides Object.prototype.hasOwnProperty
  hasOwnProperty: true,
};
// Doesn’t work in this case:
assert.throws(
  () => obj.hasOwnProperty('anyPropKey'),
  {
    name: 'TypeError',
    message: 'obj.hasOwnProperty is not a function',
  }
);
// Safe way of using .hasOwnProperty():
assert.equal(
  Object.prototype.hasOwnProperty.call(obj, 'anyPropKey'), false
);

29.9 FAQ: Klassen

29.9.1 Warum werden sie in diesem Buch „Instanz-private Felder“ und nicht „private Instanzfelder“ genannt?

Dies geschieht, um hervorzuheben, wie unterschiedlich Eigenschaften (öffentliche Slots) und private Slots sind: Durch Ändern der Reihenfolge der Adjektive werden die Wörter „öffentlich“ und „Feld“ sowie die Wörter „privat“ und „Feld“ immer zusammen genannt.

29.9.2 Warum das Bezeichnerpräfix #? Warum private Felder nicht über private deklarieren?

Könnten private Felder über private deklariert und normale Bezeichner verwendet werden? Betrachten wir, was passieren würde, wenn das möglich wäre:

class MyClass {
  private value; // (A)
  compare(other) {
    return this.value === other.value;
  }
}

Immer wenn ein Ausdruck wie other.value im Body von MyClass erscheint, muss JavaScript entscheiden:

Zur Kompilierzeit weiß JavaScript nicht, ob die Deklaration in Zeile A für other gilt (weil es eine Instanz von MyClass ist) oder nicht. Das lässt zwei Optionen für die Entscheidungsfindung:

  1. .value wird immer als privates Feld interpretiert.
  2. JavaScript entscheidet zur Laufzeit.
    • Wenn other eine Instanz von MyClass ist, wird .value als privates Feld interpretiert.
    • Andernfalls wird .value als Eigenschaft interpretiert.

Beide Optionen haben Nachteile.

Deshalb wurde das Namenspräfix # eingeführt. Die Entscheidung ist nun einfach: Wenn wir # verwenden, wollen wir auf ein privates Feld zugreifen. Wenn wir es nicht tun, wollen wir auf eine Eigenschaft zugreifen.

private funktioniert in statisch typisierten Sprachen (wie TypeScript), weil sie zur Kompilierzeit wissen, ob other eine Instanz von MyClass ist, und .value dann als privat oder öffentlich behandeln können.

  Quiz

Siehe Quiz-App.