JavaScript bietet die Möglichkeiten für objektorientierte Programmierung (OOP). In der OOP möchtest du Objekte (Instanzen) aus „Vorlagen“ (Klassen) erstellen, damit sie bestimmte Daten und Funktionalität enthalten. Die Dateneigenschaften heißen im OOP-Kontext Felder, die Funktionseigenschaften heißen Methoden.
JavaScript hatte gar keine Klassen, bevor sie 2015 in die Sprachspezifikation aufgenommen wurden, aber es ermöglichte objektorientierte Programmierung mit prototypbasierter Vererbung.
Und obwohl heutzutage ein class-Schlüsselwort verfügbar ist, ist JavaScript immer noch eine prototypbasierte Sprache.
Um zu verstehen, was es bedeutet, eine prototypbasierte Sprache zu sein, und wie JavaScript tatsächlich funktioniert, gehen wir zurück in die Zeit, als es noch keine Klassen gab.
In JavaScript wird die Vorlage (Klasse) durch eine reguläre Funktion umgesetzt.
Wenn eine Funktion als solche Vorlage verwendet werden soll, nennt man sie Konstruktorfunktion, und die Konvention ist, dass der Funktionsname mit einem Großbuchstaben beginnen sollte.
Instanzen (Objekte) werden mit dem new-Schlüsselwort aus der Vorlage abgeleitet, wenn die Konstruktorfunktion aufgerufen wird.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
Es ist wichtig zu beachten, dass in JavaScript die Instanzen und die Konstruktorfunktion auch nach der Erstellung der Instanzen eine Beziehung zueinander behalten.
Jedes Instanzobjekt enthält eine versteckte, interne Eigenschaft, die in der Sprachspezifikation [[prototype]] genannt wird.
Sie enthält eine Referenz auf den Wert des prototype-Schlüssels der Konstruktorfunktion.
Ja, du hast richtig gelesen, eine JavaScript-Funktion kann Schlüssel-Wert-Paare haben, weil sie hinter den Kulissen auch ein Objekt ist.
Seit 2015 kann auf [[prototype]] über Object.getPrototypeOf() zugegriffen werden.
Davor war es in vielen Umgebungen über den Schlüssel __proto__ zugänglich.
Verwechsle nicht den Prototyp eines Objekts ([[prototype]]) mit der prototype-Eigenschaft der Konstruktorfunktion.
Zusammenfassung:
prototype-Eigenschaft der Konstruktorfunktion wird zum Prototyp der Instanz.Oft möchtest du, dass alle abgeleiteten Objekte (Instanzen) bestimmte Felder enthalten, und beim Erzeugen des Objekts Anfangswerte für diese übergeben.
Das lässt sich über das this-Schlüsselwort umsetzen.
Innerhalb der Konstruktorfunktion repräsentiert this das neue Objekt, das über new erstellt wird.
this wird automatisch von der Konstruktorfunktion zurückgegeben, wenn sie mit new aufgerufen wird.
Das heißt, wir können der neuen Instanz Felder hinzufügen, indem wir sie in der Konstruktorfunktion zu this hinzufügen.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
Methoden werden über die prototype-Eigenschaft der Konstruktorfunktion hinzugefügt.
Innerhalb einer Methode kannst du über this auf die Felder der Instanz zugreifen.
Das funktioniert aufgrund der folgenden allgemeinen Regel.
Wenn eine Funktion als Methode eines Objekts aufgerufen wird, wird ihr
thisauf das Objekt gesetzt, auf dem die Methode aufgerufen wird. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
myCar im obigen Beispiel ist ein reguläres JavaScript-Objekt, und wenn wir es untersuchen würden (z. B. in der Browser-Konsole), würden wir keine Eigenschaft startEngine mit einer Funktion als Wert direkt im myCar-Objekt finden.
Wie funktioniert der obige Code dann überhaupt?
Das Geheimnis dahinter heißt Prototypenkette.
Wenn du auf eine beliebige Eigenschaft (Feld oder Methode) eines Objekts zugreifen willst, prüft JavaScript zuerst, ob der jeweilige Schlüssel direkt im Objekt selbst zu finden ist.
Wenn nicht, sucht es weiter nach dem Schlüssel in dem Objekt, auf das die [[prototype]]-Eigenschaft des ursprünglichen Objekts verweist.
Wie bereits erwähnt, verweist [[prototype]] in unserem Beispiel auf die prototype-Eigenschaft der Konstruktorfunktion.
Dort würde JavaScript die startEngine-Funktion finden, weil wir sie dort hinzugefügt haben.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
Und die Kette endet nicht dort.
Die [[prototype]]-Eigenschaft von Car.prototype (myCar.[[prototype]].[[prototype]]) verweist auf Object.prototype (die prototype-Eigenschaft der Object-Konstruktorfunktion).
Sie enthält allgemeine Methoden, die für alle JavaScript-Objekte verfügbar sind, z. B. toString().
Der [[prototype]] von Object ist normalerweise null, sodass die Prototypenkette dort endet.
Zusammenfassend kannst du myCar.toString() aufrufen, und diese Methode wird existieren, weil JavaScript die gesamte Prototypenkette nach dieser Methode durchsucht.
Ein ausführliches Beispiel findest du im MDN-Artikel „Vererbung und die Prototypenkette“.
Beachte, dass die Prototypenkette nur beim Abrufen eines Werts durchlaufen wird. Das direkte Setzen oder Löschen einer Eigenschaft eines Instanzobjekts betrifft nur diese spezifische Instanz. Das entspricht möglicherweise nicht deinen Erwartungen, wenn du an eine Sprache mit klassenbasierter Vererbung gewöhnt bist.
JavaScript erlaubt es dir, allen bestehenden Instanzen Methoden hinzuzufügen, selbst nachdem sie erstellt wurden.
Wir haben gelernt, dass jede Instanz eine Referenz auf die prototype-Eigenschaft der Konstruktorfunktion behält.
Das heißt, wenn du einen Eintrag zu diesem prototype-Objekt hinzufügst, ist dieser neue Eintrag (z. B. eine neue Methode) sofort für alle Instanzen verfügbar, die aus dieser Konstruktorfunktion erstellt wurden.
function Car() {
this.engineRunning = false;
}
const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
myCar.startEngine();
// This works, even though myCar was created before the method
// was added.
Theoretisch können dynamische Methoden sogar verwendet werden, um die Funktionalität von eingebauten Objekten wie Object oder Array zu erweitern, indem ihr Prototyp geändert wird.
Das nennt man Monkey Patching.
Da diese Änderung die gesamte Anwendung betrifft, sollte sie vermieden werden, um unbeabsichtigte Nebenwirkungen zu verhindern.
Der einzige sinnvolle Anwendungsfall ist, einen Polyfill für eine fehlende Methode in älteren Umgebungen bereitzustellen.
Heutzutage unterstützt JavaScript das Definieren von Klassen mit einem class-Schlüsselwort.
Dies wurde 2015 in die Sprachspezifikation aufgenommen.
Einerseits bietet dies syntaktischen Zucker, der Klassen leichter lesbar und schreibbar macht.
Die neue Syntax ähnelt mehr der Art und Weise, wie Klassen in Sprachen wie C++ oder Java geschrieben werden.
Entwickler, die von diesen Sprachen umsteigen, können sich leichter anpassen.
Andererseits ebnet die Klassen-Syntax den Weg für neue Sprachfeatures, die in der Prototyp-Syntax nicht verfügbar sind.
Mit der neuen Syntax werden Klassen mit dem class-Schlüsselwort definiert, gefolgt vom Namen der Klasse und dem Klassenkörper in geschweiften Klammern.
Der Körper enthält die Definition der Konstruktorfunktion, also eine spezielle Methode mit dem Namen constructor.
Diese Funktion funktioniert genauso wie die Konstruktorfunktion in der Prototyp-Syntax.
Der Klassenkörper enthält außerdem alle Methodendefinitionen.
Die Syntax für die Methoden ähnelt der Kurzschreibweise, die wir beim Hinzufügen von Funktionen als Werte in einem Objekt kennengelernt haben, siehe Konzept Objects.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
Ähnlich wie Funktionsdeklarationen und Funktionsausdrücke unterstützt JavaScript zusätzlich zur oben gezeigten Klassendeklaration auch Klassenausdrücke.
Bedenke, dass JavaScript hinter den Kulissen immer noch eine prototypbasierte Sprache ist. Alle Mechanismen, die wir im obigen Abschnitt „Prototyp-Syntax“ kennengelernt haben, gelten weiterhin.
Standardmäßig sind alle Instanzfelder in JavaScript öffentlich. Auf sie kann direkt zugegriffen und ihnen kann direkt zugewiesen werden.
Echte private Felder in die Sprachspezifikation aufzunehmen ist noch in Arbeit, siehe das Vorschlagsdokument für Details.
In der Zwischenzeit kannst du die etablierte Konvention nutzen, dass Felder und Methoden, die mit einem Unterstrich beginnen, als privat behandelt werden sollten. Auf sie sollte niemals direkt von außerhalb der Klasse zugegriffen werden.
Private Felder werden manchmal von Gettern und Settern begleitet.
Mit den Schlüsselwörtern get und set kannst du Funktionen definieren, die ausgeführt werden, wenn auf eine Eigenschaft mit demselben Namen wie die Funktion zugegriffen oder ihr zugewiesen wird.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
In der OOP möchtest du manchmal Hilfsfelder oder -methoden bereitstellen, die nicht von der spezifischen Instanz abhängen.
Stattdessen sind sie für die Klasse selbst definiert.
Dies lässt sich mit dem static-Schlüsselwort erreichen.
class Car {
static type = 'vehicle';
static isType(targetType) {
return targetType === 'vehicle';
}
}
Car.type;
// => 'vehicle'
Car.isType('road sign');
// => false
Neben der Art von Vererbung entlang der Prototypenkette, die wir zuvor gesehen haben, kannst du in JavaScript auch die Vererbung zwischen Klassen darstellen. Dies wird im Konzept Inheritance behandelt.
this-Beispiele - Als Objektmethode, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (abgerufen am 3. Dezember 2021) ↩