Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten.Erfahre mehr über dieses Experiment.
Verwendung von Klassen
JavaScript ist eine prototypbasierte Sprache — das Verhalten eines Objekts wird durch seine eigenen Eigenschaften und die Eigenschaften seines Prototyps spezifiziert. Mit der Einführung vonKlassen folgt die Erstellung von Objekt-Hierarchien und die Vererbung von Eigenschaften und deren Werten jedoch viel mehr dem Ansatz anderer objektorientierter Sprachen wie Java. In diesem Abschnitt zeigen wir, wie Objekte aus Klassen erstellt werden können.
In vielen anderen Sprachen werdenKlassen oder Konstruktoren klar vonObjekten oder Instanzen unterschieden. In JavaScript sind Klassen in erster Linie eine Abstraktion über den bestehenden prototypischen Vererbungsmechanismus — alle Muster sind in eine auf Prototypen basierte Vererbung umwandelbar. Klassen selbst sind ebenfalls normale JavaScript-Werte und haben ihre eigenen Prototypketten. 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.
Wir werden in diesem Tutorial mit dem gut abstrahierten Klassenmodell spielen und diskutieren, welche Semantik Klassen bieten. Wenn Sie tief in das zugrunde liegende Prototypsystem eintauchen möchten, können Sie denLeitfaden "Vererbung und die Prototypenkette" lesen.
Dieses Kapitel setzt voraus, dass Sie bereits mit JavaScript vertraut sind und gewöhnliche Objekte verwendet haben.
In diesem Artikel
Überblick über Klassen
Wenn Sie praktische Erfahrungen mit JavaScript gesammelt haben oder dem Leitfaden gefolgt sind, haben Sie wahrscheinlich bereits Klassen verwendet, selbst wenn Sie keine erstellt haben. Zum Beispiel könnte Ihnen diesbekannt vorkommen:
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 und siebigDay genannt. In der zweiten Zeile haben wir eineMethodetoLocaleDateString() auf derbigDay-Instanz aufgerufen, die eine Zeichenfolge zurückgibt. Dann verglichen wir zwei Zahlen: eine, die von dergetTime()-Methode zurückgegeben wurde, die andere direkt von derDate-Klasseselbst aufgerufen, alsDate.now().
Date ist eine eingebaute Klasse von JavaScript. Aus diesem Beispiel können wir einige grundlegende Ideen darüber ableiten, was Klassen tun:
- Klassen erstellen Objekte durch den
newOperator. - 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 üblicherweise zur Interaktion mit Instanzen verwendet werden.
Diese entsprechen den drei Schlüsselmerkmalen von Klassen:
- Konstruktor;
- Instanzmethoden und Instanzfelder;
- Statische Methoden und statische Felder.
Deklaration einer Klasse
Klassen werden normalerweise mitKlassendeklarationen erstellt.
class MyClass { // class body...}Innerhalb eines Klassenkörpers stehen eine Reihe von Funktionen zur Verfügung.
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 Welt vor ES6 kommen, sind Sie möglicherweise eher mit der Verwendung von Funktionen als Konstruktoren vertraut. Das obige Muster würde in etwa wie folgt mit Funktionskonstruktoren übersetzt werden:
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 Funktionen in Klassen, die keine einfache Entsprechung in Funktionskonstruktoren haben.
Konstruktion einer Klasse
Nachdem eine Klasse deklariert wurde, können Sie Instanzen davon mit demnew-Operator erstellen.
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 zu "aufrufen", führt jedoch zu einem Fehler.
const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'Klassen-Deklarations-Hoisting
Im Gegensatz zu Funktionsdeklarationen werden Klassendeklarationen nichtgehoisted (oder, nach 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 ist.
new MyClass(); // ReferenceError: Cannot access 'MyClass' before initializationclass MyClass {}Dieses Verhalten ähnelt Variablen, die mitlet undconst deklariert wurden.
Klassenausdrücke
Ähnlich wie Funktionen haben Klassendeklarationen auch ihre Ausdrucksgegenstücke.
const MyClass = class { // Class body...};Klassenausdrücke können ebenfalls Namen haben. Der Name des Ausdrucks ist nur für den Körper der Klasse sichtbar.
const MyClass = class MyClassLongerName { // Class body. Here MyClass and MyClassLongerName point to the same class.};new MyClassLongerName(); // ReferenceError: MyClassLongerName is not definedKonstruktor
Vielleicht ist die wichtigste Aufgabe einer Klasse, als "Fabrik" für Objekte zu fungieren. Zum Beispiel erwarten wir beim Verwenden desDate-Konstruktors, dass er ein neues Objekt liefert, das die Datendaten, die wir übergeben haben, darstellt — die wir dann mit anderen Methoden manipulieren können, die die Instanz bereitstellt. In Klassen wird die Instanzerstellung vomKonstruktor durchgeführt.
Als Beispiel würden wir eine Klasse namensColor erstellen, die eine bestimmte Farbe darstellt. Benutzer erstellen Farben, indem sie einRGB-Triplet übergeben.
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:
const red = new Color(255, 0, 0);console.log(red);Sie sollten eine Ausgabe wie diese 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 von Ihnen übergebenen RGB-Werte ist. Das entspricht ziemlich dem Folgenden:
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, dass Sie andere Syntaxen verwenden können, wieRestparameter:
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.
const red = new Color(255, 0, 0);const anotherRed = new Color(255, 0, 0);console.log(red === anotherRed); // falseInnerhalb 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).
Der Wert vonthis wird automatisch als Ergebnis vonnew zurückgegeben. Es wird empfohlen, keinen Wert aus dem Konstruktor zurückzugeben — denn wenn Sie einen nicht-primären Wert zurückgeben, wird dieser zum Wert desnew-Ausdrucks, und der Wert vonthis wird verworfen. (Sie können mehr darüber lesen, wasnew tut, inseiner Beschreibung.)
class MyClass { constructor() { this.myField = "foo"; return {}; }}console.log(new MyClass().myField); // undefinedInstanzmethoden
Wenn eine Klasse nur einen Konstruktor hat, unterscheidet sie sich nicht viel von einercreateX-Fabrikfunktion, die einfach nur einfache Objekte erstellt. Die Stärke von Klassen liegt jedoch darin, dass sie als "Vorlagen" verwendet werden können, die automatisch Methoden zu Instanzen zuweisen.
Zum Beispiel fürDate-Instanzen können Sie eine Reihe von Methoden verwenden, um verschiedene Informationen aus einem einzigen Datumswert zu erhalten, wie dasJahr,Monat,Tag der Woche 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 Rotwert der Farbe zurückgibt.
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()); // 255Ohne Methoden könnten Sie versucht sein, die Funktion im Konstruktor zu definieren:
class Color { constructor(r, g, b) { this.values = [r, g, b]; this.getRed = function () { return this.values[0]; }; }}Auch das funktioniert. Ein Problem dabei ist jedoch, dass diese Funktion jedes Mal eine neue erstellt, wenn eineColor-Instanz erstellt wird, selbst wenn sie alle dasselbe tun!
console.log(new Color().getRed === new Color().getRed); // falseIm Gegensatz dazu wird eine Methode, wenn Sie sie verwenden, zwischen allen Instanzen geteilt. Eine Funktion kann zwischen allen Instanzen geteilt werden, aber ihr Verhalten kann abweichen, wenn verschiedene Instanzen sie aufrufen, weil der Wert vonthis unterschiedlich ist. Wenn Sie neugierig sind,wo diese Methode gespeichert ist — sie ist im Prototyp aller Instanzen definiert oderColor.prototype, was im LeitfadenVererbung und die Prototypenkette ausführlicher erklärt wird.
Ähnlich können wir eine neue Methode namenssetRed erstellen, die den Rotwert der Farbe setzt.
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, diegetRed- undsetRed-Methoden zu verwenden, wenn wir direkt auf dasvalues-Array der Instanz zugreifen können?
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]); // 0Es gibt eine Philosophie in der objektorientierten Programmierung, die als "Kapselung" bezeichnet wird. Damit ist gemeint, dass Sie nicht auf die zugrunde liegende Implementierung eines Objekts zugreifen sollten, sondern stattdessen gut abstrahierte Methoden verwenden, um mit ihm zu interagieren. Zum Beispiel, wenn wir plötzlich entscheiden würden, Farben alsHSL statt in RGB darzustellen:
class Color { constructor(r, g, b) { // values is now an HSL array! this.values = rgbToHSL([r, g, b]); } getRed() { return hslToRGB(this.values)[0]; } setRed(value) { const rgb = hslToRGB(this.values); rgb[0] = value; this.values = rgbToHSL(rgb); }}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 0Die Annahme des Benutzers, dassvalues die RGB-Werte bedeuten, bricht plötzlich zusammen und könnte dazu führen, dass ihre Logik fehlerhaft wird. Wenn Sie also Implementierer einer Klasse sind, möchten Sie die interne Datenstruktur Ihrer Instanz vor Ihrem Benutzer verbergen, sowohl um die API sauber zu halten als auch um zu verhindern, dass der Code des Benutzers beim Durchführen von "harmlosen Refactorings" kaputt geht. In Klassen wird dies durchprivate Felder erreicht.
Ein privates Feld ist ein Bezeichner, der mit# (dem Rautezeichen) vorangestellt ist. Die Raute ist ein integraler Bestandteil des Feldnamens, was bedeutet, dass ein privates Feld niemals Namenskonflikte mit einem öffentlichen Feld oder einer Methode haben kann. Um in irgendeinem Teil der Klasse auf ein privates Feld zu verweisen, müssen Sie es im Klassenkörperdeklarieren (Sie können kein privates Element spontan erstellen). Davon abgesehen ist ein privates Feld so ziemlich gleichwertig mit einer normalen Eigenschaft.
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()); // 255Der Zugriff auf private Felder außerhalb der Klasse führt zu einem frühen Syntaxfehler. Die Sprache kann dies verhindern, weil#privateField eine spezielle Syntax ist, sodass sie statische Analysen durchführen und alle Verwendungen privater Felder finden kann, bevor der Code überhaupt ausgewertet wird.
console.log(red.#values); // SyntaxError: Private field '#values' must be declared in an enclosing classHinweis:Code, der in der Chrome-Konsole ausgeführt wird, kann private Elemente außerhalb der Klasse zugreifen. Dies ist eine DevTools-spezifische Lockerung der JavaScript-Syntaxbeschränkung.
Private Felder in JavaScript sindhart privat: Wenn die Klasse keine Methoden implementiert, die diese privaten Felder offenlegen, gibt es absolut keinen Mechanismus, um sie von außerhalb der Klasse abzurufen. Das bedeutet, dass Sie sicher sind, dass Sie jede beliebige Refaktorisierung an den privaten Feldern Ihrer Klasse durchführen können, solange das Verhalten der offen gelegten Methoden gleich bleibt.
Nachdem wir dasvalues-Feld privat gemacht haben, können wir etwas mehr Logik in dengetRed- undsetRed-Methoden hinzufügen, anstatt sie einfache Durchgangsmethoden sein zu lassen. Beispielsweise können wir insetRed eine Überprüfung hinzufügen, um zu sehen, ob es sich um einen gültigen R-Wert handelt:
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 valueWenn wir dievalues-Eigenschaft ungeschützt lassen, können unsere Benutzer diese Überprüfung leicht umgehen, indem sie direktvalues[0] zuweisen und ungültige Farben erzeugen. Aber mit einer gut gekapselten API können wir unseren Code robuster machen und Logikfehler weiter unten verhindern.
Eine Klassenmethode kann die privaten Felder anderer Instanzen lesen, solange sie zur gleichen Klasse gehören.
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); // 35Allerdings, wennanotherColor keine Color-Instanz ist, wird#values nicht existieren. (Selbst wenn eine andere Klasse ein identisch benanntes#values-privates Feld hat, verweist es nicht auf dasselbe und kann hier nicht zugegriffen werden.) Der Zugriff auf ein nicht vorhandenes privates Element wirft einen Fehler anstelle vonundefined wie normale Eigenschaften zurückzugeben. Wenn Sie nicht wissen, ob ein privates Feld auf einem Objekt existiert und es zugreifen möchten, ohnetry/catch zu verwenden, um den Fehler zu behandeln, können Sie denin-Operator verwenden.
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 Identifiersyntax ist und Sie den Feldnamen nicht so verwenden können, alsof wäre es eine Zeichenfolge."#values" in anotherColor würde nach einem Eigenschaftsnamen suchen, der wörtlich"#values" lautet, anstatt eines privaten Feldes.
Es gibt einige Einschränkungen bei der Verwendung privater Elemente: Der gleiche Name kann nicht zweimal innerhalb einer Klasse deklariert werden und sie können nicht gelöscht werden. Beides führt zu frühen Syntaxfehlern.
class BadIdeas { #firstName; #firstName; // syntax error occurs here #lastName; constructor() { delete this.#lastName; // also a syntax error }}Methoden,Getter und Setter können auch privat sein. Sie sind nützlich, wenn Sie etwas Komplexes haben, das die Klasse intern tun muss, aber kein anderer Teil des Codes darf es aufrufen.
Zum Beispiel, stellen Sie sich vor, Sie erstellenHTML-Benutzerelemente, die etwas etwas kompliziertes tun sollten, wenn sie angeklickt oder auf andere Weise aktiviert werden. Zudem sollten die etwas komplizierten Dinge, die passieren, wenn das Element angeklickt wird, auf diese Klasse beschränkt bleiben, denn kein anderer Teil des JavaScript wird (oder sollte) jemals darauf zugreifen.
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 im Wesentlichen fast alle Felder und Methoden privat zur Klasse. Somit präsentiert es eine Schnittstelle für den restlichen Code, die im Wesentlichen genau wie ein eingebautes HTML-Element ist. Kein anderer Teil des Programms hat die Befugnis, die Interna vonCounter zu beeinflussen.
Zugriffsorach Eigenschaften
color.getRed() undcolor.setRed() erlauben uns, den Rotwert einer Farbe zu lesen und zu schreiben. Wenn Sie aus Sprachen wie Java kommen, sind Sie mit diesem Muster sehr vertraut. Allerdings ist es in JavaScript immer noch etwas unergonomisch, Methoden zu verwenden, um einfach auf eine Eigenschaft zuzugreifen.Zugriffsorach Eigenschaften ermöglichen es uns, etwas so zu manipulieren, als ob es eine "tatsächliche Eigenschaft" wäre.
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); // 0Es sieht so aus, als ob das Objekt eine Eigenschaft namensred hat — tatsächlich existiert keine solche Eigenschaft auf der Instanz! Es gibt nur zwei Methoden, aber sie sind mitget undset versehen, was erlaubt, sie zu manipulieren, als ob sie Eigenschaften wären.
Wenn ein Feld nur über einen Getter verfügt, aber über keinen Setter, wird es effektiv schreibgeschützt sein.
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); // 255Imstrikten Modus wird die Zeilered.red = 0 eine Typfehlermeldung werfen: "Cannot set property red of #<Color> which has only a getter". Im nicht-strengen Modus wird die Zuordnung stillschweigend ignoriert.
Öffentliche Felder
Private Felder haben auch ihre öffentlichen Gegenstücke, die es jeder Instanz erlauben, eine Eigenschaft zu haben. Felder werden meist so gestaltet, dass sie unabhängig von den Parametern des Konstruktors sind.
class MyClass { luckyNumber = Math.random();}console.log(new MyClass().luckyNumber); // 0.5console.log(new MyClass().luckyNumber); // 0.3Öffentliche Felder sind fast gleichwertig mit dem Zuweisen einer Eigenschaft zuthis. Zum Beispiel lässt sich das obige Beispiel auch in Folgendes umwandeln:
class MyClass { constructor() { this.luckyNumber = Math.random(); }}Statische Eigenschaften
Mit demDate-Beispiel haben wir auch dieDate.now()-Methode kennengelernt, die das aktuelle Datum zurückgibt. Diese Methode gehört nicht zu einer beliebigen Instanz — sie gehört zur Klasse selbst. Sie ist jedoch auf derDate-Klasse platziert, anstatt als globaleDateNow()-Funktion offen zu liegen, weil sie hauptsächlich nützlich ist, wenn mit Datumsinstanzen gearbeitet wird.
Hinweis:Das Präfixieren von Hilfsmethoden mit dem, was sie behandeln, wird "Namespacing" genannt und gilt als gute Praxis. Beispielsweise hat JavaScript neben der älteren ungeprefixten MethodeparseInt() später die vorgeführteNumber.parseInt()-Methode hinzugefügt, um anzuzeigen, dass sie für Zahlen gedacht ist.
Statische Eigenschaften sind eine Gruppe von Klassenfunktionen, die auf der Klasse selbst definiert sind, statt auf einzelnen Instanzen der Klasse. Dazu 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:
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); // falseStatische Eigenschaften sind ihren Instanzgegenstücken sehr ähnlich, außer dass:
- Sie alle mit
staticvorangestellt werden und - Sie nicht aus Instanzen zugänglich sind.
console.log(new Color(0, 0, 0).isValid); // undefinedEs gibt auch eine spezielle Konstruktion namensstatischer Initialisierungsblock, welches ein Block von Code ist, der beim ersten Laden der Klasse ausgeführt wird.
class MyClass { static { MyClass.myStaticProperty = "foo"; }}console.log(MyClass.myStaticProperty); // 'foo'Statische Initialisierungsblöcke sind fast gleichwertig mit dem sofortigen Ausführen von etwas Code, nachdem eine Klasse deklariert wurde. Einziger Unterschied ist, dass sie auf statische private Elemente zugreifen können.
Erweitern und Vererbung
Ein Schlüsselelement, das Klassen mit sich bringen (zusätzlich zur ergonomischen Kapselung mit privaten Feldern), istVererbung, was bedeutet, dass ein Objekt einen Großteil des Verhaltens eines anderen Objekts "ausleihen" kann, während es bestimmte Teile mit seiner eigenen Logik überschreibt oder verbessert.
Zum Beispiel, nehmen wir an, unsereColor-Klasse muss jetzt Transparenz unterstützen. Wir könnten versucht sein, ein neues Feld hinzuzufügen, das dessen Transparenz anzeigt:
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; }}Das bedeutet jedoch, dass jede Instanz — selbst die überwiegende Mehrheit, die nicht transparent ist (diejenigen mit einem Alpha-Wert von 1) — den zusätzlichen Alpha-Wert haben muss, was nicht sehr elegant ist. Plus, wenn die Merkmale weiter wachsen, wird unsereColor klasse sehr aufgebläht sein und schwer zu warten.
Stattdessen würde man im objektorientierten Design eineabgeleitete Klasse erstellen. Die abgeleitete Klasse hat Zugriff auf alle öffentlichen Eigenschaften der Basisklasse. In JavaScript werden abgeleitete Klassen mit einerextends-Klausel deklariert, die die Klasse angibt, von der sie ableiten.
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; }}Einige Dinge fallen sofort auf. Zuerst rufen wir im Konstruktorsuper(r, g, b) auf. Es ist eine Anforderung der Sprache,super() aufzurufen, bevor aufthis zugegriffen wird. Dersuper()-Aufruf ruft den Konstruktor der Basisklasse auf, umthis zu initialisieren — hier ist es in etwa äquivalent zuthis = new Color(r, g, b). Sie können vorsuper() Code haben, aber Sie können nicht aufthis zugreifen, bevorsuper() aufgerufen wurde — die Sprache verhindert den Zugriff auf ein nicht initialisiertesthis.
Nachdem die Basisklasse mit der Modifikation vonthis fertig ist, kann die abgeleitete Klasse ihre eigene Logik ausführen. Hier haben wir ein privates Feld namens#alpha hinzugefügt und auch ein Paar Getter/Setter bereitgestellt, um damit zu interagieren.
Eine abgeleitete Klasse erbt alle Methoden von ihrer Basisklasse. Zum Beispiel, nehmen wir denget red()-Accessor, den wir zurColor im AbschnittZugriffsorach Eigenschaften hinzugefügt haben — obwohl wir keinen inColorWithAlpha deklariert haben, können wirred trotzdem zugreifen, weil dieses Verhalten von der Basisklasse angegeben wird:
const color = new ColorWithAlpha(255, 0, 0, 0.5);console.log(color.red); // 255Abgeleitete Klassen können auch Methoden von der Basisklasse überschreiben. Zum Beispiel erben alle Klassen implizit dieObject-Klasse, die einige grundlegende Methoden wietoString() definiert. Die grundlegendetoString()-Methode ist jedoch bekanntermaßen nutzlos, da sie in den meisten Fällen[object Object] druckt:
console.log(red.toString()); // [object Object]Stattdessen kann unsere Klasse sie überschreiben, um die RGB-Werte der Farbe zu drucken:
class Color { #values; // … toString() { return this.#values.join(", "); }}console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'Innerhalb abgeleiteter Klassen können Sie die Methoden der Basisklasse mitsuper aufrufen. Dadurch können Sie Verbesserungsmethoden erstellen und Code-Duplizierung vermeiden.
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 auch diese überschreiben oder verbessern können.
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)); // falseAbgeleitete Klassen haben keinen Zugriff auf die privaten Felder der Basisklasse — dies ist ein weiteres Schlüsselelement von JavaScript-privaten Feldern, die "hart privat" sind. Private Felder sind auf den Klassenkörper selbst beschränkt und gewähren keinen Zugang zuirgendeinem externen Code.
class ColorWithAlpha extends Color { log() { console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class }}Eine Klasse kann nur eine andere Klasse erweitern. Dies verhindert Probleme bei der Mehrfachvererbung wie dasDiamantproblem. Aufgrund der dynamischen Natur von JavaScript ist es jedoch dennoch möglich, den Effekt der Mehrfachvererbung durch Klassenkomposition undMixins zu erreichen.
Instanzen abgeleiteter Klassen sind auchInstanzen von der Basisklasse.
const color = new ColorWithAlpha(255, 0, 0, 0.5);console.log(color instanceof Color); // trueconsole.log(color instanceof ColorWithAlpha); // trueWarum Klassen?
Der Leitfaden war bisher pragmatisch: Wir konzentrieren uns darauf,wie Klassen verwendet werden können, aber eine Frage bleibt unbeantwortet:warum sollte man eine Klasse verwenden? Die Antwort lautet: 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 (insbesondereSubtypen-Polymorphismus) basiert. Viele Menschen sind jedoch aus philosophischen Gründen gegen bestimmte OOP-Praktiken und verwenden daher keine Klassen.
Zum Beispiel ist eine Sache, dieDate-Objekte berüchtigt macht, dass sieänderbar sind.
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Änderbarkeit und interner Zustand sind wichtige Aspekte der objektorientierten Programmierung, aber sie machen Code oft schwer zu verstehen — weil jede scheinbar harmlose Operation unerwartete Seiteneffekte haben und das Verhalten in anderen Teilen des Programms verändern kann.
Um Code wiederzuverwenden, greifen wir normalerweise darauf zurück, Klassen zu erweitern, was große Hierarchien von Vererbungsmustern erstellen kann.

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 erreicht; in JavaScript kann es durch Mixins geschehen. Aber am Ende des Tages ist es immer noch nicht sehr bequem.
Auf der positiven Seite sind Klassen eine sehr mächtige Möglichkeit, unseren Code auf höherer Ebene zu organisieren. Zum Beispiel, ohne dieColor-Klasse könnten wir ein Dutzend von Hilfsfunktionen erstellen müssen:
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. Darüber hinaus ermöglicht die Einführung von privaten Feldern, bestimmte Daten vor nachgelagerten Benutzern zu verbergen, was eine saubere API schafft.
Im Allgemeinen sollten Sie in Betracht ziehen, Klassen zu verwenden, wenn Sie Objekte erstellen möchten, die ihre eigenen internen Daten speichern und viel Verhalten bereitstellen. Nehmen Sie eingebaute JavaScript-Klassen als Beispiele:
- Die Klassen
MapundSetspeichern eine Sammlung von Elementen und ermöglichen den Zugriff auf sie per Schlüssel überget(),set(),has()usw. - Die Klasse
Datespeichert ein Datum als Unix-Zeitstempel (eine Zahl) und ermöglicht es Ihnen, Datumskomponenten zu formatieren, zu aktualisieren und auszulesen. - Die Klasse
Errorspeichert Informationen über eine bestimmte Ausnahme, einschließlich der Fehlermeldung, des Stack-Traces, der Ursache usw. Es ist eine der wenigen Klassen, die sich durch eine umfassende Vererbungsstruktur auszeichnet: Es gibt mehrere eingebaute Klassen wieTypeErrorundReferenceError, die vonErrorabgeleitet sind. Im Fall von Fehlern ermöglicht diese Vererbung die Verfeinerung der Semantik von Fehlern: Jede Fehlerklasse repräsentiert einen bestimmten Fehlertyp, der mitinstanceofeinfach überprüft werden kann.
JavaScript bietet den Mechanismus, Ihren Code in einer kanonischen objektorientierten Weise zu organisieren, aber ob und wie Sie ihn verwenden, liegt ganz im Ermessen des Programmierers.