Prototypes – Programmieren lernen mit JavaScript – Thytos
Nächstes Video startet in 3 Sekunden.
Programmieren lernen mit JavaScript

Prototypes

Die meis­ten ob­jekt­orien­tier­ten Pro­gram­mier­spra­chen sind klas­sen­ba­siert. Ja­va­Script ist ei­ne klas­sen­lo­se Spra­che. Statt ei­ner Klas­se gibt es hier ein Pro­to­typ, ein Ob­jekt, das alle In­stan­zen re­fe­ren­zie­ren.

Eine Konstruktorfunktion, die ihrer Instanz einen Wert zuweist, tut dies für jede einzelne Instanz. Das lässt sich sehr gut im folgenden Beispiel sehen, in dem eine Konstruktorfunktion eine Zufallszahl generiert und der Instanz als Property zuweist.

function Beispiel () {
  this.number = Math.random();
}

var bsp1 = new Beispiel();
var bsp2 = new Beispiel();

bsp1.number;
// => 0.32177416598766206

bsp2.number;
// => 0.44546715970437445

Die beiden Zahlen sind unterschiedlich, weil bei jeder Instanziierung der Code der Konstruktorfunktion durchlaufen und somit jedes Mal eine neue Zufallszahl erstellt wird.

Manchmal ist es aber gar nicht nötig, dass jede Instanz ihren eigenen Wert hat – vor allem Funktionen sind in der Regel bei allen Instanzen gleich und könnten deswegen am besten gemeinsam genutzt werden.
Im folgenden Beispiel erstellt eine Konstruktorfunktion über this.speak die gleiche Methode bei jeder Instanziierung neu.

// Beispiel-Konstruktor
function Fox() {
  // An der Instanz (this) wird eine Methode definiert
  this.speak = function () {
    // Bei jeder Instanziierung wird diese Methode
    // neu erstellt
    return [
      "Ring-ding-ding-ding-dingeringeding!",
      "Wa-pa-pa-pa-pa-pa-pow!",
      "Hatee-hatee-hatee-ho!",
      "Joff-tchoff-tchoffo-tchoffo-tchoff!"
    ][parseInt(Math.random() * 4)];
  };
}

// Zwei Instanzen haben dadurch zwar die
// *gleiche*, nicht aber *dieselbe* Methode
var fox1 = new Fox();
var fox2 = new Fox();

// Wäre es *dieselbe* Methode, müsste der
// Vergleich true ergeben
fox1.speak === fox2.speak
// => false

Jede erstellte Funktion verbraucht auch Arbeitsspeicher und die Erstellung einer Funktion kostet Zeit. Zwar sind Speicherverbrauch und Ausführungsdauer so gering, dass bei ein- oder zwei Instanziierungen kein Nutzer etwas merken würde, bei ein- oder zweihunderttausend Instanziierungen dagegen könnte es hier bereits zu einem merkbaren Leistungsdefizit kommen.
Besser wäre eine Lösung, bei der alle Instanzen auf ein- und dieselbe Funktion zugreifen würden. Die wäre einmalig erstellt und könnte einfach bei Bedarf auf die jeweilige Instanz angewendet werden.

Das ist möglich in JavaScript. Konstruktorfunktionen besitzen hier einen Prototyp. Werte, die im Prototyp definiert sind, werden von allen Instanzen referenziert und somit gemeinsam genutzt.

function Beispiel () {} // Hier passiert nichts mehr

// Die Zufallszahl wird nun im Prototyp definiert
Beispiel.prototype.number = Math.random();

var bsp1 = new Beispiel();
var bsp2 = new Beispiel();

// Die Zufallszahl ist nun in beiden Instanzen
// dieselbe, weil sie aus dem Prototyp kommt
bsp1.number;
// => 0.3471410494248055

bsp2.number;
// => 0.3471410494248055

Statt dass bei jeder Instanziierung eine neue Zufallszahl generiert wird, wurde sie nun einmalig im Prototyp erstellt. Alle Instanzen von Beispiel lesen dadurch die Zahl aus dem Prototyp aus, sodass die Zahl bei allen Instanzen dieselbe ist.

Dieser Mechanismus lässt sich auch auf das Beispiel mit dem Fox-Konstruktor anwenden. Statt im Konstruktor kann die Methode über Fox.prototype im Prototyp definiert werden. Dadurch nutzen alle Instanzen dieselbe Methode.

function Fox() {}

// Zugriff auf den Prototyp
Fox.prototype.speak = function () {
  // Die Methode selbst ist genauso wie vorher
  return [
    "Ring-ding-ding-ding-dingeringeding!",
    "Wa-pa-pa-pa-pa-pa-pow!",
    "Hatee-hatee-hatee-ho!",
    "Joff-tchoff-tchoffo-tchoffo-tchoff!"
  ][parseInt(Math.random() * 4)];
};

fox1 = new Fox();
fox2 = new Fox();

// Die beiden Instanzen nutzen nun *dieselbe* Methode
fox1.speak === fox2.speak
// => true

Prototyp- vs. Klassenbasierte Programmiersprache

JavaScript ist eine prototypbasierte Programmiersprache. Das ist allerdings nicht der Regelfall. Die meisten objektorientierten Programmiersprachen sind klassenbasiert.

In JavaScript referenzieren alle Objekte den Prototyp ihres Konstruktors und Eigenschaften und Methoden können daraus am Objekt ausgelesen und aufgerufen werden.
In klassenbasierten Programmiersprachen gibt es das nicht. Dort beschreibt eine Klasse die Eigenschaften und Methoden, die eine Instanz haben sollte. Bei der Instanziierung werden all diese Eigenschaften und Methoden direkt und ausschließlich an der Instanz definiert. Es gibt kein Prototyp, aus dem weitere Werte kommen könnten.

Die Prototypen in JavaScript können beliebig erweitert und verändert werden. Dadurch werden auch die bereits erstellten Instanzen beeinflusst: Wird eine neue Methode bei einem Prototyp hinzugefügt, ist diese auch über die Instanzen abrufbar.
In einer klassenbasierten Sprache geht das nicht. Einmal erstellt, können die Objekte nicht mehr verändert oder erweitert werden.

Das Prototype-Objekt

Der Zugriff auf das Prototype-Objekt erfolgt über die prototype-Property der Konstruktorfunktion.
Eigenschaften und Methoden können aus dem Prototype-Objekt ausgelesen und gesetzt werden. Es kann aber auch ein ganz eigenes Objekt dem prototype zugewiesen werden.

// Beispiel-Konstruktor
function Beispiel () {}

// Prototyp-Objekt wird über die prototype
// Property an der Konstruktorfunktion erreicht
Beispiel.prototype

var bsp = new Beispiel();

// Werte können im Prototype gesetzt und
// ausgelesen werden
Beispiel.prototype.myProperty = "My value";

// Prototype-Properties werden von den
// Instanzen des Konstruktors, zu dem sie
// gehören, referenziert
bsp.myProperty
// => "My value"

// Alternativ kann auch ein ganz neues
// Objekt als Prototyp gesetzt werden
Beispiel.prototype = {
  "ein ganz": "neues Objekt"
};

// Wichtig: Das setzt prototype auf eine
// neue Referenz. Bereits erstellte
// Instanzen referenzieren weiterhin das
// alte Prototype-Objekt mit dessen Werten
bsp.myProperty // Hat noch immer einen Wert
// => "My value"

bsp["ein ganz"] // Funktioniert dagegen nicht
// => undefined

Eigene und vererbte Werte

In manchen Fällen ist es durchaus sinnvoll, einen Wert nicht im Prototyp zu definieren, denn nicht jeder Wert sollte immer mit allen Instanzen geteilt werden.
Ein Fall wäre der Datenbank-Konstruktor aus dem letzten Video: In der Konstruktorfunktion wurden sämtliche Properties der Instanz erstellt.

function Database() {
  this.datensatz = [];
  this.insert = function (obj) {…};
  this.where = function (prop, val) {…};
  this.update = function () {…};
  this.delete = function (prop, val) {…};
}

Was passiert, wenn all diese Properties in das Prototype-Objekt verschoben würden?

function Database() {}
Database.prototype = {
  datensatz: [],
  insert: function (obj) {…},
  where: function (prop, val) {…},
  update: function () {…},
  delete: function (prop, val) {…}
};

var db = new Database();

db.where(); // Die Datenbank ist leer
// => []

db.insert({ "hallo": "welt" });

db.where();
// => [{ hallo: "welt" }]

Es scheint alles zu funktionieren. Doch was passiert, wenn eine zweite Instanz erstellt wird?

var db2 = new Database();

db2.where(); // Der Datensatz ist derselbe wie bei db1
// => [{ hallo: "welt" }]

Dadurch dass die Werte des Prototype-Objekts von allen Instanzen gemeinsam genutzt wird, wird auch der Datensatz gemeinsam genutzt. Eine Datenbank sollte allerdings einen eigenen Datensatz haben.
Deswegen ist es sinnvoll, die datensatz-Property wie gehabt im Konstruktor zu definieren, während die Methoden weiterhin im Prototype definiert sein können.

function Database() {
  // Jede Instanz sollte ihren eigenen Datensatz haben
  this.datensatz = [];
}
Database.prototype = {
  // datensatz wird nicht mehr im Prototyp definiert
  insert: function (obj) {…},
  where: function (prop, val) {…},
  update: function () {…},
  delete: function (prop, val) {…}
};