Was sind eigentlich Prototypen in JavaScript und TypeScript?

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}`;
};
Prototype Chain für das erste Code-Beispiel

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.

Prototype Chain in JavaScript

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
Prototype Chain für das Musician Code Beispiel

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 oder Twitter.