Movatterモバイル変換


[0]ホーム

URL:


MDN Web Docs

Experiment: Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten.

Verwendung von Klassen

JavaScript ist eine prototypbasierte Sprache — das Verhalten eines Objekts wird durch seine eigenen Eigenschaften und die Eigenschaften seines Prototyps bestimmt. Mit der Einführung vonKlassen ist die Erstellung von Objekt-Hierarchien und die Vererbung von Eigenschaften und deren Werten jedoch viel mehr im Einklang mit anderen objektorientierten Sprachen wie Java. In diesem Abschnitt werden wir demonstrieren, wie Objekte aus Klassen erstellt werden können.

In vielen anderen Sprachen sindKlassen oder Konstruktoren klar vonObjekten oder Instanzen zu unterscheiden. In JavaScript sind Klassen hauptsächlich eine Abstraktion über den bestehenden prototypischen Vererbungsmechanismus — alle Patterns sind auf Prototyp-Vererbung übertragbar. Klassen selbst sind ebenfalls normale JavaScript-Werte und haben ihre eigenen Prototyp-Ketten. Tatsächlich können die meisten einfachen JavaScript-Funktionen als Konstruktoren verwendet werden — Sie verwenden dennew-Operator mit einer Konstruktorfunktion, um ein neues Objekt zu erstellen.

In diesem Tutorial werden wir uns mit dem gut abstrahierten Klassenmodell beschäftigen und diskutieren, welche Semantik Klassen bieten. Wenn Sie tief in das zugrunde liegende Prototyp-System eintauchen möchten, können Sie denVererbungs- und Prototyp-Ketten-Leitfaden lesen.

Dieses Kapitel setzt voraus, dass Sie bereits mit JavaScript vertraut sind und gewöhnliche Objekte verwendet haben.

Überblick über Klassen

Wenn Sie praktische Erfahrung mit JavaScript haben oder dem Leitfaden gefolgt sind, haben Sie wahrscheinlich bereits Klassen verwendet, auch wenn Sie keine erstellt haben. Zum Beispiel könnte Ihnen dasbekannt vorkommen:

js
const bigDay = new Date(2019, 6, 19);console.log(bigDay.toLocaleDateString());if (bigDay.getTime() < Date.now()) {  console.log("Once upon a time...");}

In der ersten Zeile haben wir eine Instanz der KlasseDate erstellt, die wirbigDay genannt haben. In der zweiten Zeile haben wir eineMethodetoLocaleDateString() auf derbigDay-Instanz aufgerufen, die einen String zurückgibt. Dann haben wir zwei Zahlen verglichen: eine, die von dergetTime()-Methode zurückgegeben wurde, und die andere, die direkt von derDate-Klasseselbst aufgerufen wurde, alsDate.now().

Date ist eine eingebaute Klasse von JavaScript. Aus diesem Beispiel können wir einige grundlegende Vorstellungen davon gewinnen, was Klassen tun:

  • Klassen erstellen Objekte über dennew-Operator.
  • Jedes Objekt hat einige Eigenschaften (Daten oder Methoden), die von der Klasse hinzugefügt werden.
  • Die Klasse speichert selbst einige Eigenschaften (Daten oder Methoden), die normalerweise zur Interaktion mit Instanzen verwendet werden.

Diese entsprechen den drei Schlüsselfunktionen von Klassen:

  • Konstruktor;
  • Instanzmethoden und Instanzfelder;
  • Statische Methoden und statische Felder.

Deklarieren einer Klasse

Klassen werden normalerweise mitKlassendeklarationen erstellt.

js
class MyClass {  // class body...}

Innerhalb eines Klassenkörpers stehen eine Reihe von Funktionen zur Verfügung.

js
class MyClass {  // Constructor  constructor() {    // Constructor body  }  // Instance field  myField = "foo";  // Instance method  myMethod() {    // myMethod body  }  // Static field  static myStaticField = "bar";  // Static method  static myStaticMethod() {    // myStaticMethod body  }  // Static block  static {    // Static initialization code  }  // Fields, methods, static fields, and static methods all have  // "private" forms  #myPrivateField = "bar";}

Wenn Sie aus einer Zeit vor ES6 kommen, sind Sie vielleicht eher mit der Verwendung von Funktionen als Konstruktoren vertraut. Das obige Pattern würde in etwa folgendermaßen mit Funktionskonstruktoren übersetzt werden:

js
function MyClass() {  this.myField = "foo";  // Constructor body}MyClass.myStaticField = "bar";MyClass.myStaticMethod = function () {  // myStaticMethod body};MyClass.prototype.myMethod = function () {  // myMethod body};(function () {  // Static initialization code})();

Hinweis:Private Felder und Methoden sind neue Merkmale in Klassen ohne triviale Entsprechung in Funktionskonstruktoren.

Klasse konstruieren

Nachdem eine Klasse deklariert wurde, können Sie Instanzen davon mithilfe desnew-Operators erstellen.

js
const myInstance = new MyClass();console.log(myInstance.myField); // 'foo'myInstance.myMethod();

Typische Funktionskonstruktoren können sowohl mitnew konstruiert als auch ohnenew aufgerufen werden. Der Versuch, eine Klasse ohnenew aufzurufen, führt jedoch zu einem Fehler.

js
const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'

Klassendeklaration Hoisting

Im Gegensatz zu Funktionsdeklarationen werden Klassendeklarationen nichtgehoben (oder, in einigen Interpretationen, gehoben, aber mit der Einschränkung der temporären Totzone), was bedeutet, dass Sie eine Klasse nicht verwenden können, bevor sie deklariert wird.

js
new MyClass(); // ReferenceError: Cannot access 'MyClass' before initializationclass MyClass {}

Dieses Verhalten ähnelt Variablen, die mitlet undconst deklariert wurden.

Klassenausdrücke

Ähnlich wie bei Funktionen haben Klassendeklarationen auch Ausdrucksgegenstücke.

js
const MyClass = class {  // Class body...};

Klassen-Ausdrücke können auch Namen haben. Der Name des Ausdrucks ist nur für den Körpers der Klasse sichtbar.

js
const MyClass = class MyClassLongerName {  // Class body. Here MyClass and MyClassLongerName point to the same class.};new MyClassLongerName(); // ReferenceError: MyClassLongerName is not defined

Konstruktor

Vielleicht ist die wichtigste Aufgabe einer Klasse, als "Fabrik" für Objekte zu fungieren. Wenn wir zum Beispiel denDate-Konstruktor verwenden, erwarten wir, dass er ein neues Objekt gibt, das die Datum-Daten darstellt, die wir eingegeben haben — die wir dann mit anderen Methoden bearbeiten können, die die Instanz bereitstellt. In Klassen wird die Instanzerstellung durch denKonstruktor vorgenommen.

Als Beispiel würden wir eine Klasse namensColor erstellen, die eine bestimmte Farbe darstellt. Benutzer erstellen Farben, indem sie einRGB-Triplet eingeben.

js
class Color {  constructor(r, g, b) {    // Assign the RGB values as a property of `this`.    this.values = [r, g, b];  }}

Öffnen Sie die Entwicklertools Ihres Browsers, fügen Sie den obigen Code in die Konsole ein und erstellen Sie dann eine Instanz:

js
const red = new Color(255, 0, 0);console.log(red);

Sie sollten eine Ausgabe ähnlich dieser sehen:

Object { values: (3) […] }  values: Array(3) [ 255, 0, 0 ]

Sie haben erfolgreich eineColor-Instanz erstellt, und die Instanz hat einevalues-Eigenschaft, die ein Array der RGB-Werte ist, die Sie eingegeben haben. Das entspricht in etwa dem Folgenden:

js
function createColor(r, g, b) {  return {    values: [r, g, b],  };}

Die Syntax des Konstruktors ist genau die gleiche wie bei einer normalen Funktion — das bedeutet, Sie können andere Syntaxen wieRestparameter verwenden:

js
class Color {  constructor(...values) {    this.values = values;  }}const red = new Color(255, 0, 0);// Creates an instance with the same shape as above.

Jedes Mal, wenn Sienew aufrufen, wird eine andere Instanz erstellt.

js
const red = new Color(255, 0, 0);const anotherRed = new Color(255, 0, 0);console.log(red === anotherRed); // false

Innerhalb eines Klassenkonstruktors zeigt der Wert vonthis auf die neu erstellte Instanz. Sie können ihm Eigenschaften zuweisen oder vorhandene Eigenschaften lesen (insbesondere Methoden — die wir als nächstes behandeln werden).

Derthis-Wert wird automatisch als Ergebnis vonnew zurückgegeben. Es wird empfohlen, keinen Wert aus dem Konstruktor zurückzugeben — denn wenn Sie einen nicht-primitiven Wert zurückgeben, wird er zum Wert desnew-Ausdrucks und der Wert vonthis wird verworfen. (Sie können mehr darüber lesen, wasnew tut, inseiner Beschreibung.)

js
class MyClass {  constructor() {    this.myField = "foo";    return {};  }}console.log(new MyClass().myField); // undefined

Instanzmethoden

Wenn eine Klasse nur einen Konstruktor hat, unterscheidet sie sich nicht wesentlich von einercreateX Fabrik-Funktion, die nur einfache Objekte erstellt. Der Vorteil von Klassen besteht jedoch darin, dass sie als "Vorlagen" verwendet werden können, die automatisch Methoden an Instanzen zuweisen.

Zum Beispiel können Sie beiDate-Instanzen eine Reihe von Methoden verwenden, um unterschiedliche Informationen aus einem einzelnen Datumswert zu erhalten, wie dasJahr, denMonat, denWochentag, usw. Sie können diese Werte auch über diesetX-Gegenstücke wiesetFullYear setzen.

Für unsere eigeneColor-Klasse können wir eine Methode namensgetRed hinzufügen, die den roten Wert der Farbe zurückgibt.

js
class Color {  constructor(r, g, b) {    this.values = [r, g, b];  }  getRed() {    return this.values[0];  }}const red = new Color(255, 0, 0);console.log(red.getRed()); // 255

Ohne Methoden könnten Sie versucht sein, die Funktion im Konstruktor zu definieren:

js
class Color {  constructor(r, g, b) {    this.values = [r, g, b];    this.getRed = function () {      return this.values[0];    };  }}

Dies funktioniert auch. Ein Problem ist jedoch, dass dies jedes Mal eine neue Funktion erstellt, wenn eineColor-Instanz erstellt wird, auch wenn sie alle das gleiche tun!

js
console.log(new Color().getRed === new Color().getRed); // false

Im Gegensatz dazu wird, wenn Sie eine Methode verwenden, diese zwischen allen Instanzen geteilt. Eine Funktion kann zwischen allen Instanzen geteilt werden, aber dennoch ihr Verhalten ändern, wenn verschiedene Instanzen sie aufrufen, da der Wert vonthis unterschiedlich ist. Wenn Sie neugierig sind,wo diese Methode gespeichert ist — sie ist im Prototyp aller Instanzen oderColor.prototype definiert, was im Detail imVererbungs- und Prototyp-Ketten-Leitfaden erklärt wird.

Ähnlich können wir eine neue Methode namenssetRed erstellen, die den roten Wert der Farbe setzt.

js
class Color {  constructor(r, g, b) {    this.values = [r, g, b];  }  getRed() {    return this.values[0];  }  setRed(value) {    this.values[0] = value;  }}const red = new Color(255, 0, 0);red.setRed(0);console.log(red.getRed()); // 0; of course, it should be called "black" at this stage!

Private Felder

Sie fragen sich vielleicht: Warum sollten wir uns die Mühe machen,getRed undsetRed Methoden zu verwenden, wenn wir direkt auf dasvalues-Array in der Instanz zugreifen können?

js
class Color {  constructor(r, g, b) {    this.values = [r, g, b];  }}const red = new Color(255, 0, 0);red.values[0] = 0;console.log(red.values[0]); // 0

Es gibt eine Philosophie in der objektorientierten Programmierung, die als "Kapselung" bezeichnet wird. Das bedeutet, dass Sie nicht auf die zugrunde liegende Implementierung eines Objekts zugreifen sollten, sondern stattdessen gut abstrahierte Methoden verwenden sollten, um mit ihm zu interagieren. Wenn wir zum Beispiel plötzlich entscheiden, Farben alsHSL statt als RGB darzustellen:

js
class Color {  constructor(r, g, b) {    // values is now an HSL array!    this.values = rgbToHSL([r, g, b]);  }  getRed() {    return this.values[0];  }  setRed(value) {    this.values[0] = value;  }}const red = new Color(255, 0, 0);console.log(red.values[0]); // 0; It's not 255 anymore, because the H value for pure red is 0

Die Annahme des Benutzers, dassvalues die RGB-Werte bedeutet, bricht plötzlich zusammen, und es könnte dazu führen, dass ihre Logik nicht mehr korrekt funktioniert. Wenn Sie also Implementierer einer Klasse sind, möchten Sie die interne Datenstruktur Ihrer Instanz vor Ihrem Benutzer verbergen, um sowohl die API sauber zu halten als auch zu verhindern, dass der Code des Benutzers zusammenbricht, wenn Sie einige "harmlose Refaktorisierungen" vornehmen. In Klassen wird dies durchprivate Felder erreicht.

Ein privates Feld ist ein Bezeichner, dem ein# (Das Rautezeichen) vorangestellt ist. Die Raute ist ein integraler Bestandteil des Feldnamens, was bedeutet, dass ein privates Feld niemals einen Namenkonflikt mit einem öffentlichen Feld oder einer Methode haben kann. Um auf ein privates Feld irgendwo in der Klasse zu verweisen, müssen Sie esim Klassenkörper deklarieren (Sie können kein privates Element im Handumdrehen erstellen). Abgesehen davon ist ein privates Feld nahezu gleichwertig mit einer normalen Eigenschaft.

js
class Color {  // Declare: every Color instance has a private field called #values.  #values;  constructor(r, g, b) {    this.#values = [r, g, b];  }  getRed() {    return this.#values[0];  }  setRed(value) {    this.#values[0] = value;  }}const red = new Color(255, 0, 0);console.log(red.getRed()); // 255

Der Zugriff auf private Felder außerhalb der Klasse ist ein früher Syntaxfehler. Die Sprache kann dies verhindern, weil#privateField eine spezielle Syntax ist, sodass sie einige statische Analysen durchführen und alle Verwendungen privater Felder finden kann, bevor der Code überhaupt ausgewertet wird.

js
console.log(red.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class

Hinweis:Code, der in der Chrome-Konsole ausgeführt wird, kann auf private Elemente außerhalb der Klasse zugreifen. Dies ist eine nur in den Entwicklertools geltende Entspannung der JavaScript-Syntaxeinschränkung.

Private Felder in JavaScript sindhart privat: Wenn die Klasse keine Methoden implementiert, die diese privaten Felder freigeben, gibt es absolut keinen Mechanismus, um sie außerhalb der Klasse zu erhalten. Das bedeutet, dass es sicher ist, beliebige Refaktorisierungen der privaten Felder Ihrer Klasse vorzunehmen, solange das Verhalten der freigelegten Methoden gleich bleibt.

Nachdem wir das Feldvalues privat gemacht haben, können wir etwas mehr Logik in diegetRed undsetRed Methoden einfügen, anstatt sie einfache Durchreichmethoden sein zu lassen. Wir können zum Beispiel eine Überprüfung insetRed hinzufügen, um zu sehen, ob es sich um einen gültigen R-Wert handelt:

js
class Color {  #values;  constructor(r, g, b) {    this.#values = [r, g, b];  }  getRed() {    return this.#values[0];  }  setRed(value) {    if (value < 0 || value > 255) {      throw new RangeError("Invalid R value");    }    this.#values[0] = value;  }}const red = new Color(255, 0, 0);red.setRed(1000); // RangeError: Invalid R value

Wenn wir dievalues-Eigenschaft freilegen, können unsere Benutzer diese Überprüfung leicht umgehen, indem sie direkt aufvalues[0] zuweisen und ungültige Farben erstellen. Aber mit einer gut gekapselten API können wir unseren Code robuster machen und Logikfehler nachgelagerter Prozesse verhindern.

Eine Klassenmethode kann die privaten Felder anderer Instanzen lesen, solange sie zur gleichen Klasse gehören.

js
class Color {  #values;  constructor(r, g, b) {    this.#values = [r, g, b];  }  redDifference(anotherColor) {    // #values doesn't necessarily need to be accessed from this:    // you can access private fields of other instances belonging    // to the same class.    return this.#values[0] - anotherColor.#values[0];  }}const red = new Color(255, 0, 0);const crimson = new Color(220, 20, 60);red.redDifference(crimson); // 35

WennanotherColor jedoch keine Color-Instanz ist, existiert#values nicht. (Selbst wenn eine andere Klasse ein gleichnamiges#values privates Feld hat, bezieht es sich nicht auf dasselbe und kann hier nicht zugegriffen werden.) Der Zugriff auf ein nicht existentes privates Element führt zu einem Fehler, anstattundefined wie normale Eigenschaften zurückzugeben. Wenn Sie nicht wissen, ob ein privates Feld in einem Objekt existiert und Sie ohnetry/catch darauf zugreifen möchten, um den Fehler zu behandeln, können Sie denin-Operator verwenden.

js
class Color {  #values;  constructor(r, g, b) {    this.#values = [r, g, b];  }  redDifference(anotherColor) {    if (!(#values in anotherColor)) {      throw new TypeError("Color instance expected");    }    return this.#values[0] - anotherColor.#values[0];  }}

Hinweis:Beachten Sie, dass# eine spezielle Identifikatorsyntax ist und Sie den Feldnamen nicht verwenden können, als wäre es ein String."#values" in anotherColor würde nach einer Eigenschaft namens" #values" suchen, anstatt nach einem privaten Feld.

Es gibt einige Einschränkungen bei der Verwendung privater Elemente: Der gleiche Name kann nicht zweimal in einer einzelnen Klasse deklariert werden, und sie können nicht gelöscht werden. Beides führt zu frühen Syntaxfehlern.

js
class BadIdeas {  #firstName;  #firstName; // syntax error occurs here  #lastName;  constructor() {    delete this.#lastName; // also a syntax error  }}

Methoden,Getter und Setter können ebenfalls privat sein. Sie sind nützlich, wenn Sie etwas Komplexes haben, das die Klasse intern tun muss, aber kein anderer Teil des Codes darauf zugreifen darf.

Stellen Sie sich zum Beispiel vor, dassHTML-benutzerdefinierte Elemente erstellt werden, die etwas kompliziertes tun sollen, wenn sie angeklickt/getippt oder anderweitig aktiviert werden. Darüber hinaus sollten die komplizierten Dinge, die passieren, wenn das Element angeklickt wird, auf diese Klasse beschränkt sein, da kein anderer Teil des JavaScript darauf zugreifen (oder darauf zugreifen dürfen sollte).

js
class Counter extends HTMLElement {  #xValue = 0;  constructor() {    super();    this.onclick = this.#clicked.bind(this);  }  get #x() {    return this.#xValue;  }  set #x(value) {    this.#xValue = value;    window.requestAnimationFrame(this.#render.bind(this));  }  #clicked() {    this.#x++;  }  #render() {    this.textContent = this.#x.toString();  }  connectedCallback() {    this.#render();  }}customElements.define("num-counter", Counter);

In diesem Fall sind praktisch alle Felder und Methoden privat für die Klasse. Dadurch wird eine Schnittstelle zum Rest des Codes bereitgestellt, die im Wesentlichen wie ein eingebautes HTML-Element aussieht. Kein anderer Teil des Programms hat die Macht, eines der internen Elemente vonCounter zu beeinflussen.

Zugriffs-Felder

color.getRed() undcolor.setRed() ermöglichen uns das Lesen und Schreiben des roten Werts einer Farbe. Wenn Sie aus Sprachen wie Java kommen, werden Sie sehr vertraut mit diesem Muster sein. Der Gebrauch von Methoden zum einfachen Zugriff auf eine Eigenschaft ist jedoch in JavaScript immer noch etwas unergonomisch.Accessor Fields (Zugriffs-Felder) ermöglichen es uns, etwas zu manipulieren, als ob es eine "tatsächliche Eigenschaft" wäre.

js
class Color {  constructor(r, g, b) {    this.values = [r, g, b];  }  get red() {    return this.values[0];  }  set red(value) {    this.values[0] = value;  }}const red = new Color(255, 0, 0);red.red = 0;console.log(red.red); // 0

Es sieht so aus, als hätte das Objekt eine Eigenschaft namensred — aber tatsächlich gibt es keine solche Eigenschaft auf der Instanz! Es gibt nur zwei Methoden, aber sie sind mitget undset vorangehend, wodurch sie manipuliert werden können, als ob sie Eigenschaften wären.

Wenn ein Feld nur über einen Getter, aber keinen Setter verfügt, wird es effektiv schreibgeschützt.

js
class Color {  constructor(r, g, b) {    this.values = [r, g, b];  }  get red() {    return this.values[0];  }}const red = new Color(255, 0, 0);red.red = 0;console.log(red.red); // 255

ImStrict Mode wird die Zeilered.red = 0 einen Typfehler auslösen: "Eigenschaft red von #<Color> kann nicht gesetzt werden, da sie nur einen Getter hat". Im Nicht-Strict-Mode wird die Zuweisung stillschweigend ignoriert.

Öffentliche Felder

Private Felder haben auch ihre öffentlichen Gegenstücke, die es jeder Instanz ermöglichen, eine Eigenschaft zu haben. Felder sind in der Regel so konzipiert, dass sie unabhängig von den Parametern des Konstruktors sind.

js
class MyClass {  luckyNumber = Math.random();}console.log(new MyClass().luckyNumber); // 0.5console.log(new MyClass().luckyNumber); // 0.3

Öffentliche Felder sind fast gleichbedeutend mit dem Zuordnen einer Eigenschaft zuthis. Zum Beispiel kann das obige Beispiel auch in folgendes umgewandelt werden:

js
class MyClass {  constructor() {    this.luckyNumber = Math.random();  }}

Statische Eigenschaften

Mit demDate-Beispiel sind wir auch auf die MethodeDate.now() gestoßen, die das aktuelle Datum zurückgibt. Diese Methode gehört zu keiner Date-Instanz — sie gehört zur Klasse selbst. Allerdings wird sie auf derDate-Klasse platziert anstatt als globaleDateNow()-Funktion veröffentlicht zu werden, weil sie hauptsächlich dann nützlich ist, wenn man sich mit Date-Instanzen beschäftigt.

Hinweis:Das Präfixn von Hilfsmethoden mit dem, womit sie sich beschäftigen, wird "namespacing" genannt und wird als gute Praxis betrachtet. Zum Beispiel hat JavaScript zusätzlich zur älteren, nicht vorgezeichnetenparseInt() Funktion auch die vorgezeichneteNumber.parseInt() Methode hinzugefügt, um anzudeuten, dass sie sich mit Zahlen beschäftigt.

Statische Eigenschaften sind eine Gruppe von Klassenmerkmalen, die auf der Klasse selbst definiert sind, anstatt auf den einzelnen Instanzen der Klasse. Zu diesen Merkmalen gehören:

  • Statische Methoden
  • Statische Felder
  • Statische Getter und Setter

Alles hat auch private Gegenstücke. Zum Beispiel können wir für unsereColor-Klasse eine statische Methode erstellen, die überprüft, ob ein gegebenes Triplet ein gültiger RGB-Wert ist:

js
class Color {  static isValid(r, g, b) {    return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255;  }}Color.isValid(255, 0, 0); // trueColor.isValid(1000, 0, 0); // false

Statische Eigenschaften sind sehr ähnlich zu ihren Instanz-Gegenstücken, außer dass:

  • Sie alle mitstatic vorgezeichnet sind, und
  • Sie sind nicht von Instanzen aus zugänglich.
js
console.log(new Color(0, 0, 0).isValid); // undefined

Es gibt auch ein spezielles Konstrukt, das alsstatischer Initialisierungsblock bekannt ist, welcher ein Codeblock ist, der ausgeführt wird, wenn die Klasse zuerst geladen wird.

js
class MyClass {  static {    MyClass.myStaticProperty = "foo";  }}console.log(MyClass.myStaticProperty); // 'foo'

Statische Initialisierungsblöcke sind fast gleichbedeutend mit dem sofortigen Ausführen eines Codes nach einer Klassendeklaration. Der einzige Unterschied ist, dass sie Zugriff auf statische private Elemente haben.

Erweitert und Vererbung

Ein Schlüsselmerkmal, das Klassen mit sich bringen (zusätzlich zu ergonomischer Kapselung mit privaten Feldern), istVererbung, was bedeutet, dass ein Objekt einen großen Teil der Verhaltensweisen eines anderen Objekts "ausleihen" kann, während bestimmte Teile mit seiner eigenen Logik überschrieben oder erweitert werden.

Nehmen wir zum Beispiel an, unserColor-Klasse muss nun Transparenz unterstützen. Wir könnten versucht sein, ein neues Feld hinzuzufügen, das die Transparenz angibt:

js
class Color {  #values;  constructor(r, g, b, a = 1) {    this.#values = [r, g, b, a];  }  get alpha() {    return this.#values[3];  }  set alpha(value) {    if (value < 0 || value > 1) {      throw new RangeError("Alpha value must be between 0 and 1");    }    this.#values[3] = value;  }}

Dies bedeutet jedoch, dass jede Instanz — selbst die große Mehrheit, die nicht transparent ist (diejenigen mit einem Alphawert von 1) — den zusätzlichen Alpha-Wert haben muss, was nicht sehr elegant ist. Außerdem wird unsereColor-Klasse, wenn die Funktionen weiter wachsen, sehr aufgebläht und schwer zu pflegen.

Stattdessen würden wir in der objektorientierten Programmierung eineabgeleitete Klasse erstellen. Die abgeleitete Klasse hat Zugriff auf alle öffentlichen Eigenschaften der übergeordneten Klasse. In JavaScript werden abgeleitete Klassen mit einerextends Klausel deklariert, die die Klasse angibt, von der sie erbt.

js
class ColorWithAlpha extends Color {  #alpha;  constructor(r, g, b, a) {    super(r, g, b);    this.#alpha = a;  }  get alpha() {    return this.#alpha;  }  set alpha(value) {    if (value < 0 || value > 1) {      throw new RangeError("Alpha value must be between 0 and 1");    }    this.#alpha = value;  }}

Es gibt einige Dinge, die sofort auffallen. Zuerst ist, dass wir im Konstruktorsuper(r, g, b) aufrufen. Es ist eine Sprachanforderung,super() aufzurufen, bevor aufthis zugegriffen wird. Dersuper()-Aufruf ruft den Konstruktor der übergeordneten Klasse auf, umthis zu initialisieren — hier ungefähr gleichbedeutend mitthis = new Color(r, g, b). Sie können Code vorsuper() haben, aber Sie dürfen nicht aufthis zugreifen, bevorsuper() — die Sprache verhindert, dass auf das nicht initialisiertethis zugegriffen wird.

Nachdem die übergeordnete Klasse mit der Änderung vonthis fertig ist, kann die abgeleitete Klasse ihre eigene Logik anwenden. Hier haben wir ein privates Feld namens#alpha hinzugefügt und bieten auch ein Paar Getter/Setter zum Interagieren mit ihnen.

Eine abgeleitete Klasse erbt alle Methoden von ihrer Elternklasse. Zum Beispiel, obwohlColorWithAlpha keinenget red()-Accessor selbst angibt, können Sie dennoch aufred zugreifen, weil dieses Verhalten von der Elternklasse angegeben wird:

js
const color = new ColorWithAlpha(255, 0, 0, 0.5);console.log(color.red); // 255

Abgeleitete Klassen können auch Methoden von der Elternklasse überschreiben. Zum Beispiel erbt jede Klasse implizit dieObject Klasse, die einige grundlegende Methoden wietoString() definiert. Wenn jedoch die BasistoString() Methode berüchtigt nutzlos ist, weil sie in den meisten Fällen[object Object] druckt:

js
console.log(red.toString()); // [object Object]

Stattdessen kann unsere Klasse sie überschreiben, um die RGB-Werte der Farbe zu drucken:

js
class Color {  #values;  // …  toString() {    return this.#values.join(", ");  }}console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'

Innerhalb von abgeleiteten Klassen können Sie auf die Methoden der Elternklasse mitsuper zugreifen. Dies ermöglicht es Ihnen, Verbesserungsmethoden zu erstellen und Code-Duplizierung zu vermeiden.

js
class ColorWithAlpha extends Color {  #alpha;  // …  toString() {    // Call the parent class's toString() and build on the return value    return `${super.toString()}, ${this.#alpha}`;  }}console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'

Wenn Sieextends verwenden, erben auch die statischen Methoden voneinander, sodass Sie sie ebenfalls überschreiben oder verbessern können.

js
class ColorWithAlpha extends Color {  // …  static isValid(r, g, b, a) {    // Call the parent class's isValid() and build on the return value    return super.isValid(r, g, b) && a >= 0 && a <= 1;  }}console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false

Abgeleitete Klassen haben keinen Zugriff auf die privaten Felder der Elternklasse — dies ist ein weiterer Schlüsselaspekt, dass JavaScript privaten Felder "hart privat" sind. Private Felder sind auf den Klassenkörper selbst beschränkt und gewährenkeinem externen Code Zugriff.

js
class ColorWithAlpha extends Color {  log() {    console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class  }}

Eine Klasse kann nur von einer anderen Klasse erben. Dies verhindert Probleme bei der Mehrfachvererbung, wie dasDiamantproblem. Aufgrund der dynamischen Natur von JavaScript ist es jedoch immer noch möglich, den Effekt der Mehrfachvererbung durch Klassenkomposition undMixins zu erzielen.

Instanzen von abgeleiteten Klassen sind auchInstanzen von der Basisklasse.

js
const color = new ColorWithAlpha(255, 0, 0, 0.5);console.log(color instanceof Color); // trueconsole.log(color instanceof ColorWithAlpha); // true

Warum Klassen?

Der Leitfaden war bisher pragmatisch: Wir konzentrieren uns darauf,wie Klassen verwendet werden können, aber es bleibt eine Frage unbeantwortet:warum sollte man eine Klasse verwenden? Die Antwort ist: es kommt darauf an.

Klassen führen einParadigma ein, oder eine Möglichkeit, Ihren Code zu organisieren. Klassen sind die Grundlagen der objektorientierten Programmierung, die auf Konzepten wieVererbung undPolymorphismus (insbesondereUntertyp-Polymorphismus) basieren. Viele Menschen sind jedoch philosophisch gegen bestimmte OOP-Praktiken und verwenden Klassen deshalb nicht.

Zum Beispiel ist eines der Dinge, dieDate Objekte berüchtigt machen, dass sieveränderbar sind.

js
function incrementDay(date) {  return date.setDate(date.getDate() + 1);}const date = new Date(); // 2019-06-19const newDay = incrementDay(date);console.log(newDay); // 2019-06-20// The old date is modified as well!?console.log(date); // 2019-06-20

Veränderbarkeit und interner Zustand sind wichtige Aspekte der objektorientierten Programmierung, machen es jedoch oft schwierig, den Code zu begreifen — weil jede scheinbar unschuldige Operation unerwartete Nebenwirkungen haben und das Verhalten in anderen Teilen des Programms ändern kann.

Um Code wiederzuverwenden, greifen wir normalerweise darauf zurück, Klassen zu erweitern, was große Hierarchien von Vererbungsmustern schaffen kann.

Ein typischer OOP-Vererbungsbaum mit fünf Klassen und drei Ebenen

Es ist jedoch oft schwierig, Vererbung sauber zu beschreiben, wenn eine Klasse nur eine andere Klasse erweitern kann. Oft wollen wir das Verhalten mehrerer Klassen. In Java wird dies durch Schnittstellen gemacht; in JavaScript kann es durch Mixins gemacht werden. Aber letztlich ist es immer noch nicht sehr praktisch.

Auf der positiven Seite sind Klassen ein sehr mächtiges Mittel, um unseren Code auf einer höheren Ebene zu organisieren. Zum Beispiel könnten wir ohne dieColor Klasse ein Dutzend Hilfsfunktionen erstellen müssen:

js
function isRed(color) {  return color.red === 255;}function isValidColor(color) {  return (    color.red >= 0 &&    color.red <= 255 &&    color.green >= 0 &&    color.green <= 255 &&    color.blue >= 0 &&    color.blue <= 255  );}// …

Aber mit Klassen können wir sie alle unter demColor Namespace zusammenfassen, was die Lesbarkeit verbessert. Außerdem ermöglicht die Einführung von privaten Feldern, bestimmte Daten vor nachgelagerten Benutzern zu verbergen, wodurch eine saubere API geschaffen wird.

Im Allgemeinen sollten Sie in Betracht ziehen, Klassen zu verwenden, wenn Sie Objekte erstellen möchten, die ihre eigenen internen Daten speichern und ein umfangreiches Verhalten bereitstellen. Nehmen Sie eingebaute JavaScript-Klassen als Beispiele:

  • DieMap undSet Klassen speichern eine Sammlung von Elementen und ermöglichen es Ihnen, sie per Schlüssel zuzugreifen, indem Sieget(),set(),has(), usw. verwenden.
  • DieDate Klasse speichert ein Datum als Unix-Zeitstempel (eine Zahl) und ermöglicht es Ihnen, bestimmte Datumsbestandteile zu formatieren, zu aktualisieren und zu lesen.
  • DieError Klasse speichert Informationen über eine bestimmte Ausnahme, einschließlich der Fehlermeldung, des Stack-Traces, der Ursache, usw. Es ist eine der wenigen Klassen, die mit einer reichen Vererbungstruktur kommen: Es gibt mehrere eingebaute Klassen wieTypeError undReferenceError, dieError erweitern. Im Fall von Fehlern erlaubt diese Vererbung, die Semantik von Fehlern zu verfeinern: Jede Fehlerklasse repräsentiert einen bestimmten Fehlertyp, der leicht mitinstanceof überprüft werden kann.

JavaScript bietet den Mechanismus, Ihren Code in einer kanonischen objektorientierten Weise zu organisieren, jedoch liegt es ganz im Ermessen des Programmierers, ob und wie er ihn verwenden möchte.

MDN-Feedback-Box

Diese Seite wurde automatisch aus dem Englischen übersetzt.


[8]ページ先頭

©2009-2025 Movatter.jp