Was sind eigentlich Prototypen in JavaScript und TypeScript?
About 3 min reading time
Für diesen Blog möchte ich ein paar Dinge aus dem JavaScript Umfeld zusammenfassen, die mir immer wieder begegnen und stellenweise auch mal für Probleme sorgen. Prototypen sind etwas, dass immer mal wieder für Verwirrung sorgt und hier möchte ich versuchen, dass ganze verständlich zu erklären.
JavaScript ist eine prototypenbasierte Programmiersprache. Das bedeutet also zunächst, dass JavaScript Objektorientierung unterstützt. Laut Definition allerdings eine "ohne Klassen" (was nicht stimmt, mehr dazu später im Artikel).
Während in anderen Programmiersprachen Objekte durch die Instanziierung einer Klasse erzeugt werden (x = new Foo("bar")
), wird in prototypenbasierten ein Objekt durch das Klonen eines bereits existierenden Objekts instanziiert. Klonen bedeutet, dass der Klon alle Eigenschaften des Originals hat und nutzen kann.
Um zu verstehen, was dies für JavaScript bedeutet, lohnt sich ein Blick auf den ECMA Standard zum Begriff prototype:
object that provides shared properties for other objects
ECMA Standard
Ein Prototyp ist also erst mal ein Objekt in JavaScript und dieses hat Properties, also Werte und Funktionen, die es mit anderen Objekten teilen kann.
Unter der Erklärung steht auch noch eine Anmerkung:
When a constructor creates an object, that object implicitly references the constructor’s “prototype” property for the purpose of resolving property references. The constructor’s “prototype” property can be referenced by the program expression
constructor.prototype
, and properties added to an object’s prototype are shared, through inheritance, by all objects sharing the prototype. Alternatively, a new object may be created with an explicitly specified prototype by using the Object.create built-in function.
JavaScript kennt also Konstruktoren. Im "klassischen" JavaScript (besonders vor ES6) war es daher üblich, dass man mit Konstruktor Funktionen, Prototypen definieren und diese dann auch instanziieren kann.
Sehen wir uns hierzu mal ein klassisches Beispiel an:
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
const person1 = new Person('John', 'Doe');
Instanziierung
Das Objekt person1
wird mithilfe der Konstruktor Funktion instanziiert und hat fortan alle Funktionen, die der Prototyp kennt.
Die Instanziierung klappt auch noch mit der Funktion Object.create
, es sollte aber schnell ersichtlich werden, warum man das heute eher selten so macht:
const person2 = Object.create(Person.prototype, {
firstName: { value: 'Jane' },
lastName: { value: 'Doe' }
});
Trotzdem hat diese Methode natürlich auch ihre Berechtigung, da ich hier noch recht feingranular, die Eigenschaften der Objekte konfigurieren kann (bspw. read-only).
Erweiterung von Prototypen
Wenn wir später den Prototypen mit weiteren Funktionen erweitern würden, würden die Objekte auch davon profitieren (das fühlt sich ein wenig wie Extensions in Kotlin an).
Person.prototype.fullName = function () {
return `${this.firstName} ${this.lastName}`;
};
Das funktioniert auch mit den Datentypen, die JavaScript mitbringt (wie String, Number, Array, Function, ...), ist aber ein Anti-Pattern, da diese Veränderung in einer Anwendung natürlich überall passieren könnte und dies dementsprechend zu unvorhergesehenen Effekten führen kann.
Das funktioniert, da jedes hier erstellte Objekt die Eigenschaft __proto__
hat, die auf unseren Prototypen Person
verweist (Person.prototype === person1.__proto__
).
Der Wert von __proto__
in jeder Instanz des Konstruktors ist ein direkter Verweis auf den Prototyp des Konstruktors. Immer, wenn wir versuchen, auf eine Eigenschaft eines Objekts zuzugreifen, die nicht direkt auf diesem existiert, geht JavaScript die Prototype Chain hinunter, um zu sehen, ob die Eigenschaft innerhalb dieser verfügbar ist.
Diese Eigenschaft hat übrigens auch jedes andere Objekt in JavaScript. Wenn wir ein Objekt einfach erzeugen, dann ist dessen Prototyp immer Object
const person3 = {
firstName: 'Max',
lastName: 'Mustermann',
fullName: function () {
return `${this.firstName} ${this.lastName}`;
}
};
// person3.__proto__ === Object.prototype
Klassen
Heutzutage unterstützt JavaScript "Klassen", baut aber intern weiterhin auf Prototypen, weicht also ein wenig vom Lehrbuch ab. Das Beispiel von oben lässt sich also heutzutage folgendermaßen schreiben:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const person1 = new Person('John', 'Doe');
Vererbung
Wenn wir von Objektorientierung sprechen, ist auch Vererbung ein Thema. Wir haben immer mal wieder spezialisierte Objekte, die zusätzliche Funktionen haben. In klassischem JavaScript ist eine Variante, den Prototyp manuell zu setzen:
function Musician(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Object.setPrototypeOf(Musician.prototype, Person.prototype);
Musician.prototype.sing = function (song) {
return `${this.fullName()}: ${song}`;
};
Das sieht auf den ersten Blick etwas ungewöhnlich aus, erlaubt aber natürlich, sich einfach in die Prototype Chain zu hängen und Vererbung einzuführen.
Mit der Unterstützung von Klassen hat JavaScript nun aber auch die Unterstützung des Keywords extends
bekommen, womit wir die Prototype Chain zu unserm Vorteil nutzen können und gleich lesbareren Code bekommen:
class Musician extends Person {
constructor(firstName, lastName) {
super(firstName, lastName);
}
sing(song) {
return `${this.fullName()}: ${song}`;
}
}
const paul = new Musician('Paul', 'McCartney');
// paul.__proto__ === Musician.prototype
// paul.__proto__.__proto__ === Person.prototype
Ich nutze Klassen kaum in JavaScript und TypeScript. Meist nutze ich Objekte nur, um Werte zu gruppieren (value objects). Die Logik wird über Funktionen abgebildet. Wenn ich mit Klassen arbeite, liegt dies größtenteils an Frameworks. Trotzdem spiele ich immer wieder mit dem Gedanken, ob Objekte mit mehr Funktionen nicht auch nützlich sein könnten.
Wie sieht das bei Euch aus? Schreibt ihr viele Klassen oder nutzt ihr mehr Funktionen? Schreibt mir gerne auf Mastodon.