14 Prototypenbasierte Objektorientierung

JavaScript implementiert objektorientierte Programmierung auf eine einzigartige Weise: statt klassenbasierter Vererbung, wie sie in Sprachen wie Java oder C++ üblich ist, nutzt JavaScript ein prototypenbasiertes Vererbungsmodell. Dieses Konzept ist fundamental für das tiefere Verständnis der Sprache, selbst wenn moderne Syntax wie ES6-Klassen verwendet wird.

14.0.1 Grundkonzept: Objekte und Prototypen

In JavaScript ist fast alles ein Objekt. Jedes Objekt hat einen Verweis auf einen Prototypen - ein anderes Objekt, von dem es Eigenschaften und Methoden erbt. Diese Prototypen-Kette bildet das Rückgrat der Objektvererbung in JavaScript.

// Ein einfaches Objekt
const fahrzeug = {
  antrieb: 'Motor',
  fahren: function() {
    return `Fahrzeug fährt mit ${this.antrieb}`;
  }
};

// Ein weiteres Objekt, das fahrzeug als Prototyp verwendet
const auto = Object.create(fahrzeug);
auto.räder = 4;
auto.antrieb = 'Benzinmotor';

console.log(auto.fahren()); // "Fahrzeug fährt mit Benzinmotor"
console.log(auto.räder);    // 4

In diesem Beispiel: - fahrzeug ist ein Objekt mit eigenen Eigenschaften - auto ist ein neues Objekt, das fahrzeug als Prototyp hat - auto erbt die fahren()-Methode von seinem Prototyp - auto überschreibt die Eigenschaft antrieb mit einem eigenen Wert

14.0.2 Die Prototypen-Kette

Wenn auf eine Eigenschaft eines Objekts zugegriffen wird, sucht JavaScript zunächst im Objekt selbst. Wird die Eigenschaft dort nicht gefunden, erfolgt die Suche im Prototyp des Objekts, dann im Prototyp des Prototyps und so weiter, bis entweder die Eigenschaft gefunden oder das Ende der Kette (meist Object.prototype) erreicht wird.

// Die Prototypen-Kette in Aktion
console.log(auto.toString()); // Erbt von Object.prototype
console.log(auto.hasOwnProperty('räder')); // true - prüft eigene Eigenschaften
console.log(auto.hasOwnProperty('fahren')); // false - keine eigene Eigenschaft

14.0.3 Constructor-Funktionen und new-Operator

Vor ES6 war der häufigste Weg, um Objekte mit gemeinsamen Eigenschaften zu erstellen, die Verwendung von Constructor-Funktionen in Kombination mit dem new-Operator:

function Fahrzeug(antrieb) {
  this.antrieb = antrieb;
}

// Methoden zum Prototyp hinzufügen
Fahrzeug.prototype.fahren = function() {
  return `Fahrzeug fährt mit ${this.antrieb}`;
};

// Objekte mit dem Constructor erstellen
const auto = new Fahrzeug('Benzinmotor');
const motorrad = new Fahrzeug('Zweitaktmotor');

console.log(auto.fahren()); // "Fahrzeug fährt mit Benzinmotor"
console.log(motorrad.fahren()); // "Fahrzeug fährt mit Zweitaktmotor"

Was passiert beim Aufruf mit new: 1. Ein neues, leeres Objekt wird erstellt 2. Der Prototyp dieses Objekts wird auf Fahrzeug.prototype gesetzt 3. Die Funktion Fahrzeug wird mit this als das neue Objekt ausgeführt 4. Das neue Objekt wird zurückgegeben (sofern die Funktion nicht explizit etwas anderes zurückgibt)

14.0.4 Prototypische Vererbung

Die prototypische Vererbung ermöglicht es, Objekte zu erstellen, die andere Objekte erweitern:

// Basis-Constructor
function Fahrzeug(antrieb) {
  this.antrieb = antrieb;
}

Fahrzeug.prototype.starten = function() {
  return `${this.antrieb} startet`;
};

// Abgeleiteter Constructor
function Auto(antrieb, marke) {
  // "Super"-Aufruf
  Fahrzeug.call(this, antrieb);
  this.marke = marke;
  this.räder = 4;
}

// Vererbung einrichten
Auto.prototype = Object.create(Fahrzeug.prototype);
Auto.prototype.constructor = Auto; // Constructor-Referenz korrigieren

// Eigene Methoden für Auto
Auto.prototype.hupen = function() {
  return `${this.marke} hupt`;
};

const meinAuto = new Auto('Dieselmotor', 'VW');
console.log(meinAuto.starten()); // "Dieselmotor startet"
console.log(meinAuto.hupen());   // "VW hupt"

14.0.5 Das prototype Property vs. [[Prototype]]

Es ist wichtig, zwei verwandte, aber unterschiedliche Konzepte zu verstehen:

  1. Das prototype-Property einer Funktion: Wenn eine Funktion als Constructor verwendet wird, bestimmt ihr prototype-Property, welches Objekt als Prototyp für die mit new erstellten Instanzen dient.

  2. Der interne [[Prototype]]-Verweis eines Objekts: Dies ist der tatsächliche Prototyp eines Objekts, auf den über Object.getPrototypeOf() oder das nicht standardisierte __proto__ zugegriffen werden kann.

function Test() {}
const obj = new Test();

// Beziehungen zeigen
console.log(Object.getPrototypeOf(obj) === Test.prototype); // true
console.log(obj.__proto__ === Test.prototype); // true, aber __proto__ ist veraltet

14.0.6 Eigenschaftsbeschreibungen und Object.defineProperty()

JavaScript bietet präzise Kontrolle über Objekteigenschaften durch Deskriptoren:

const person = {};

Object.defineProperty(person, 'name', {
  value: 'Max',
  writable: true,      // Kann überschrieben werden
  enumerable: true,    // Erscheint in for...in Schleifen
  configurable: true   // Kann gelöscht oder rekonfiguriert werden
});

// Getter/Setter definieren
let _alter = 30;
Object.defineProperty(person, 'alter', {
  get: function() { return _alter; },
  set: function(wert) {
    if (wert > 0) _alter = wert;
  },
  enumerable: true,
  configurable: true
});

person.alter = 35;
console.log(person.alter); // 35

14.0.7 Moderne Prototyp-Manipulation

ES6+ bietet verbesserte Möglichkeiten zur Arbeit mit Prototypen:

// Object.setPrototypeOf() - nachträgliche Änderung des Prototyps
const tier = { 
  atmen: function() { return 'Tier atmet'; } 
};

const hund = { 
  bellen: function() { return 'Wau!'; } 
};

Object.setPrototypeOf(hund, tier);
console.log(hund.atmen()); // "Tier atmet"

// Object.getPrototypeOf() - sicherer Zugriff auf den Prototyp
console.log(Object.getPrototypeOf(hund) === tier); // true