Feinheiten des Objektmodells

JavaScript ist eine objektbasierte Sprache, die auf Prototypen aufbaut anstatt wie andere Sprachen auf Klassen. Aufgrund dieser unterschiedlichen Aufbauweise ist vielleicht nicht sofort ersichtlich, wie mit JavaScript Objekthierarchien zusammengestellt werden können und die Vererbung von Objekteigenschaften und Werten funktioniert. Bei diesem Kapitel wird versucht darüber Klarheit zu schaffen.

Für dieses Kapitel wird angenommen, dass der Leser bereits ein wenig Erfahrung mit JavaScript gesammelt und JavaScript-Funktionen benutzt hat, um einfache Objekte zu erstellen.

Unterschiede von klassenbasierten und objektbasierten Sprachen

Klassenbasierte objektorientierte Sprachen (wie Java oder C++) setzen auf ein Konzept mit zwei verschiedene Einheiten: Klassen und Instanzen.

  • Eine Klasse definiert alle Eigenschaften (Methoden und Felder bei Java oder Members bei C++), die einen bestimmten Satz von Objekten charakterisieren. Eine Klasse ist ein abstraktes Ding, eine Art Bauplan für ein Objekt, und keines der Objekte selbst. Zum Beispiel repräsentiert die Klasse Employee den ganzen Satz von Mitarbeitern.
  • Eine Instanz hingegen ist eine Instanzierung der Klasse, also ein Member der Klasse. Zum Beispiel könnte Viktoria eine Instanz der Klasse Employee sein und eine Mitarbeiterin eines Unternehmens repräsentieren. Eine Instanz hat genau die Eigenschaften der Elternklasse (nicht mehr und nicht weniger).

Eine prototypenbasierte Sprache wie JavaScript macht diese Unterscheidung nicht: Es gibt einfach nur Objekte. Bei einer prototypenbasierten Sprache gibt es prototypische Objekte, die vordefinierte Eigenschaften besitzen und als Vorlage für neue Objekte dienen. Darüber hinaus kann jedes Objekt eigene Eigenschaften besitzen, die entweder bei der Erstellung oder später während der Laufzeit hinzugefügt werden. Jedes beliebige Objekt kann ein Prototype für ein anderes Objekt sein, sodass sich die Objekte die Eigenschaften teilen.

Definition einer Klasse

Bei klassenbasierten Sprachen definiert man eine Klasse in einer separaten Klassendefinition. In dieser Definition können spezielle Methoden, sogenannte Konstruktoren, zur Erstellung von Klasseninstanzen definiert werden. Mit einer Konstruktormethode lassen sich Anfangswerte für die Eigenschaften der Instanzen festlegen und andere Operationen zum Zeitpunkt der Erstellung durchführen. Der Operator new kommt in Verbindung mit der Konstruktormethode für die Erstellung von Klasseninstanzen zum Einsatz.

JavaScript folgt einem ähnlichen Modell, jedoch gibt es hier keine seperate Klassendefintion unabhängig vom Konstruktor eines Objekts. Stattdessen definiert man eine Konstruktorfunktion, womit beim Aufruf Objekte mit bestimmten in der Definition festgelegten Eigenschaften und Werten erstellt werden. Jede beliebige JavaScript-Funktion kann als Konstruktor verwendet werden. Der Operator new kommt in Verbindung mit der Konstruktorfunktion für die Erstellung von Objekten zum Einsatz.

Kindklassen und Vererbung

Bei einer klassenbasierten Sprache kommt die Hierarchie von Klassen durch die Definition von Klassen zustande. Bei der Definition einer Klasse legt man fest, ob die neue Klasse eine Kindklasse von einer bereits bestehenden Klasse sein soll. Die Kindklasse erbt sodann alle Eigenschaften der Elternklasse. Zusätzlich können der Kindklasse neue Eigenschaften hinzugefügt, oder die von der Elternklasse geerbten Eigenschaften verändert werden. Nehmen wir z. B. an, die Klasse Employee würde ausschließlich Eigenschaften für den Namen und die Abteilung der jeweiligen Mitarbeiter besitzen. Dann kann man eine Klasse Manager als Kindklasse der Klasse Employee erstellen und dieser eine Eigenschaft für Berichte hinzufügen. Eine Instanz der Managerklasse würde dann alle drei Eigenschaften besitzen: Name, Abteilung und Berichte.

Hinzufügen und Entfernen von Eigenschaften

Bei klassenbasierten Sprachen erstellt man in der Regel eine Klasse während der Kompilierung und erzeugt dann Klasseninstanzen entweder bereits bei der Kompilierung oder zur Laufzeit. Nach Erstellung der Klasse können keine Eigenschaften hinzugefügt, entfernt oder die Art von Eigenschaften geändert werden. JavaScript erlaubt hingegen das Hinzufügen oder Entfernen von Eigenschaften oder Objekten auch während der Laufzeit; wird einem Objekt eine neue Eigenschaft hinzugefügt, das als Prototype für ein anderes Objekt fungiert, so erhält auch das erbende Objekt diese neue Eigenschaft.

Zusammenfassung der Unterschiede

Die folgende Tabelle gibt einen schnellen Überblick über einige der Unterschiede zwischen JavaScript und Java. Der Rest dieses Kapitels beschreibt JavaScript-Konstruktoren und Prototypen für die Erstellung von Objekthierarchien genauer und vergleicht das Konzept von JavaScript mit dem von Java.

Tabelle 8.1 Vergleich von klassenbasierten (Java) und prototypbasierten (JavaScript) objektbasierten Systemen
Klassenbasiert (Java) Prototypbasiert (JavaScript)
Klassen und Instanzen sind verschiedene Einheiten. Alle Objekte sind Instanzen.
Definition einer Klasse über eine Klassendefinition; Instanzierung von Klassen mit Hilfe von Konstruktormethoden; Definition und Erstellung von Sätzen eines Objekts mit Hilfe von Konstruktorfunktionen;
Erzeugung eines einzelnen Objekts mit dem Operator new. Genauso.
Konstruktion einer Objekthierarchie mit Klassendefinitionen und der Definition von Kindklassen; Konstruktion einer Objekthierarchie durch die Zuweisung eines Objekts als Prototype für eine Konstruktorfunktion;
Vererbung von Eigenschaften durch Klassen-Verkettung; Vererbung von Eigenschaften durch Prototype-Verkettung;
Die Klassendefinition legt alle Eigenschaften einer Klasseninstanz fest. Eigenschaften können nicht während der Laufzeit hinzugefügt werden. Die Konstruktorfunktion oder der Prototype legt einen anfänglichen Satz von Eigenschaften fest; neue Eigenschaften können dynamisch für einzelne Instanzen oder den kompletten Satz von Instanzen hinzugefügt oder entfernt werden;

Das Employee-Beispiel

Die nachfolgenden Erklärungen dieses Kapitels beziehen sich zum Teil auf das Employee-Beispiel, dessen Hierarchie wie in folgendem Bild aufgebaut ist:

Bild 8.1: Eine einfache Objekthierarchie

Diese Hierarchie setzt sich aus folgenden Objekte zusammen:

  • Employee hat die Eigenschaften name (mit einem leeren String als Anfangswert) und dept (mit dem Anfangswert "general").
  • Manager basiert auf dem Employee-Objekt und besitzt die zusätzliche Eigenschaft reports. Der Anfangswert ist ein leeres Array, das für die Speicherung von Employee-Objekten vorgesehen ist.
  • WorkerBee basiert ebenfalls auf dem Employee-Objekt und besitzt die zusätzliche Eigenschaft projects (mit einem leeren Array als Anfangswert, das für die Speicherung von Stringwerten vorgesehen ist).
  • SalesPerson basiert auf dem Objekt WorkerBee und besitzt die zusätzliche Eigenschaft quota (mit 100 als Anfangswert). Außerdem wird der Wert der Eigenschaft dept mit dem Wert "sales" überschrieben, wodurch ersichtlich werden soll, dass Verkaufsleute in derselben Abteilung beschäftigt sind.
  • Engineer basiert auf dem Objekt WorkerBee und besitzt die zusätzliche Eigenschaft machine (mit einem leeren String als Anfangswert). Der Wert der Eigenschaft dept wird mit dem Wert "engineering" überschrieben.

Erstellung der Hierarchie

Es gibt verschiedene Möglichkeiten, die entsprechenden Konstruktiorfunktionen zu definieren, um die Hierarchie zu implementieren. Welches Verfahren sich am besten eignet, hängt vom Einsatzzweck der Applikation ab.

In diesem Abschnitt wird beschrieben, wie mit sehr einfachen (jedoch relativ unflexiblen) Definitionen eine Vererbungshierarchie aufgebaut werden kann. Bei diesen Definitionen können den Eigenschaften eines Objekts bei der Erstellung keine eigenen Werte zugewiesen werden. Den neu erstellten Objekten werden Anfangswerte zugewiesen, die erst später geändert werden können. Das Bild 8.2 stellt die Hierarchie mit diesen simplen Definitionen dar.

Bei einer echten Applikation würde man evtl. Konstruktoren definieren, womit den Objekteigenschaften bei der Erzeugung selbstdefinierte Anfangswerte zugewiesen werden können; (siehe More Flexible Constructors für weitere Informationen). Zu Anfang begnügen wir uns jedoch mit einfachen Definitionen, um rein die Grundstruktur der Vererbungshierarchie zu veranschaulichen.

figure8.2.png
Bild 8.2: Die Employee-Objektdefinitionen

Die Java- und Javascript-Definitionen von Employee in der folgenden Tabelle sind gleichbedeutend. Der einzige Unterschied besteht darin, dass bei Java der Typ für jede Eigenschaft spezifiziert werden und explizit eine Konstruktormethode erstellt werden muss.

JavaScript Java
function Employee () {
  this.name = "";
  this.dept = "general";
}
public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this.name = "";
      this.dept = "general";
   }
}

Die Definitionen der Objekte Manager und WorkerBee (Bild 8.2) zeigen die Unterschiede bei der Spezifikation des übergeordneten Objekts in der Vererbungskette. Bei JavaScript weist man der Eigenschaft prototype der Konstruktorfunktion eine Prototype-Instanz als Wert zu. Dies kann jederzeit nach der Definition der Konstruktorfunktion geschehen. Bei Java gibt man die Elternklasse in der Definition an. Eine Änderung der Elternklasse ist außerhalb der Klassendefintion nicht möglich.

JavaScript Java
function Manager () {
  this.reports = [];
}
Manager.prototype = new Employee;

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;
public class Manager extends Employee {
   public Employee[] reports;
   public Manager () {
      this.reports = new Employee[0];
   }
}

public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this.projects = new String[0];
   }
}

Mit den Definitionen Engineer und SalesPerson werden Objekte erstellt, die von WorkerBee abstammen und deshalb auch von Employee, da WorkerBee von Employee erbt. Ein Objekt dieser Gruppe besitzt die Eigenschaften von allen Objekten, die in der Hierarchie übergeordnet sind. Der geerbte Wert der Eigenschaft dept wird mit einem neuen Wert überschrieben.

JavaScript Java
function SalesPerson () {
   this.dept = "sales";
   this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;

function Engineer () {
   this.dept = "engineering";
   this.machine = "";
}
Engineer.prototype = new WorkerBee;
public class SalesPerson extends WorkerBee {
   public double quota;
   public SalesPerson () {
      this.dept = "sales";
      this.quota = 100.0;
   }
}

public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      this.dept = "engineering";
      this.machine = "";
   }
}

Durch den Gebrauch dieser Definitionen können Instanzen der entsprechenden Objekte erzeugt werden, welche dann die festgelegten Anfangswerte für die Eigenschaften erhalten. Das Bild 8.3 führt die Benutzung der Definitionen für die Erstellung neuer Objekte vor und zeigt die Werte der Eigenschaften von neuen Objekten.

Hinweis: Der Begriff Instanz hat bei klassenbasierten Sprachen eine spezielle technische Bedeutung. Bei diesen Sprachen ist eine Instanz eine eigenständige Instanzierung einer Klasse und unterscheidet sich grundlegend von der Klasse selbst. Bei JavaScript gibt es diese spezielle Bedeutung nicht, da es keinen Unterschied zwischen Instanzen und Klassen gibt. Oft spricht man jedoch von Instanzen, wenn Objekte gemeint sind, die mit einer bestimmten Konstruktorfunktion erstellt wurden. Somit darf man für das Beispiel behaupten, dass jane eine Instanz von Engineer ist. Auf dieselbe Weise haben auch die Begriffe Eltern, Kind, Vorfahre und Nachkomme keine formelle Bedeutung bei JavaScript und können für Objekte benutzt werden, die anderen Objekten in der Prototypekette über- oder untergeordnet sind.

figure8.3.png
Bild 8.3: Erstellung von Objekten mit einfachen Definitionen

Objekt-Eigenschaften

Dieser Abschnitt erklärt, wie Objekte die Eigenschaften von anderen Objekten erben, die in der Prototypekette hierarchisch übergeordnet sind und was passiert, wenn neue Eigenschaften während der Laufzeit hinzugefügt werden.

Vererbung von Eigenschaften

Angenommen man erstellt das Objekt mark als WorkerBee (wie in Bild 8.3) mit der folgenden Anweisung:

var mark = new WorkerBee;

Wenn die Ausführung beim new-Operator angelangt ist, wird ein neues WorkerBee-Objekt erstellt und dieses Objekt dem this-Schlüsselwort der WorkerBee-Konstruktorfunktion zugewiesen. Die Konstruktorfunktion setzt dann explizit den Wert für die Eigenschaft projects (wie in der Konstruktorfunktion definiert) und implizit den Wert der internen Eigenschaft __proto__ auf den Wert von WorkerBee.prototype (__proto__ hat jeweils zwei Unterstriche am Anfang und Ende). Die Eigenschaft __proto__ bestimmt die Prototype-Verkettung für die Rückgabe von Eigenschaftswerten. Sobald diese Eigenschaften gesetzt sind, wird das neue Objekt zurückgegeben und der Variablen mark zugewiesen.

Bei diesem Vorgang werden dem Objekt für die Eigenschaften, die mark von der Prototypekette erbt, nicht explizit (lokale) Werte für die Eigenschaften zugewiesen. Wenn der Wert einer Eigenschaft abgefragt wird, überprüft JavaScript zunächst, ob dieser Wert lokal existiert; trifft dies zu, wird der Wert zurückgegeben. Falls die Eigenschaft lokal nicht existiert, überprüft JavaScript die Prototypekette (über die Eigenschaft __proto__) und besitzt ein Objekt in der Prototypekette einen Wert für die Eigenschaft, wird dieser zurückgegeben. Falls gar keine solche Eigenschaft gefunden wird, meldet JavaScript, dass das Objekt keine solche Eigenschaft besitzt. Demzufolge hat das Objekt mark die folgenden Eigenschaften und Werte:

mark.name = "";
mark.dept = "general";
mark.projects = [];

Das mark-Objekt erbt die Werte für die Eigenschaften name und dept vom prototypischen Objekt in mark.__proto__. Diesem wurde ein lokaler Wert für die Eigenschaft projects über die WorkerBee-Konstruktorfunktion zugewiesen. Durch dieses Gefüge wird bei JavaScript Vererbung konstruiert. Einige weitere Details zu diesem Vorgang sind im Kapitel Property Inheritance Revisited beschrieben.

Mit den Konstruktoren können keine Werte für einzelne Instanzen definiert, sondern nur allgemeine Werte für alle Objekte eines Typs festgelegt werden. Die Eigenschaftswerte sind also Default-Werte, die allen neuen WorkerBee-Objekten bei ihrer Erzeugung zugewiesen werden. Natürlich kann man die Werte der Eigenschaften jederzeit ändern und z. B. mark wie folgt einen speziellen Wert zuweisen:

mark.name = "Doe, Mark";
mark.dept = "admin";
mark.projects = ["navigator"];

Hinzufügen von Eigenschaften

Bei JavaScript kann man jedem beliebigen Objekt während der Laufzeit neue Eigenschaften hinzufügen. Dabei muss die Eigenschaft nicht zwingend durch einen Konstruktor erstellt worden sein, auch komplett neue Eigenschaften lassen sich jederzeit während der Laufzeit definieren. Um eine neue Eigenschaft spezielle für ein einzelnes Objekt zu definieren, weißt man dem Objekt wie folgt einen neuen Wert zu:

mark.bonus = 3000;

Hiernach besitzt das mark-Objekt eine Eigenschaft bonus. Die anderen WorkerBee-Objekte bleiben unverändert.

Wenn man einem Objekt, das als Prototype für eine Konstruktorfunktion dient, eine neue Eigenschaft zuweist, wird diese Eigenschaft allen Objekten hinzugefügt, die von dem Prototype erben. Zum Beispiel kann man mit der folgenden Anweisung allen Employee-Objekten die Eigenschaft specialty hinzufügen:

Employee.prototype.specialty = "none";

Sobald diese Anweisung ausgeführt wird, besitzt das Objekt mark ebenfalls die Eigenschaft speciality mit dem Wert "none". Im nächsten Bild ist dargestellt, wie dem Prototype des Employee-Objekts die neue Eigenschaft hinzugefügt und anschließend dem Engineer-Prototype ein anderer Wert zugewiesen wird.


Bild 8.4: Hinzufügen von Eigenschaften

Flexiblere Konstruktoren

Mit den bisher beschriebenen Konstruktoren lassen sich bei der Erzeugung von Instanzen keine variablen Eigenschaftswerte definieren. Wie bei Java kann man Konstruktoren jedoch auch Argumente übergeben, um mit den Argumentwerten Instanzen zu initialisieren:


Bild 8.5: Festlegen von Eigenschaften über den Konstruktor, Teil 1

Die folgende Tabelle zeigt die Java- und JavaScript-Definitionen für diese Objekte:

JavaScript Java
function Employee (name, dept) {
  this.name = name || "";
  this.dept = dept || "general";
}
public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this("", "general");
   }
   public Employee (String name) {
      this(name, "general");
   }
   public Employee (String name, String dept) {
      this.name = name;
      this.dept = dept;
   }
}
function WorkerBee (projs) {
  this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this(new String[0]);
   }
   public WorkerBee (String[] projs) {
      projects = projs;
   }
}
function Engineer (mach) {
   this.dept = "engineering";
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      dept = "engineering";
      machine = "";
   }
   public Engineer (String mach) {
      dept = "engineering";
      machine = mach;
   }
}

Diese JavaScript-Definitionen setzen eine spezielle logische Verknüpfung für Default-Werte ein:

this.name = name || "";

Der JavaScript-Operator für das logische ODER (||) wertet das ihm vorangestellte Argument aus. Wenn dieses zu true evaluiert, wird der Wert zurückgegeben. Andernfalls wird der Wert des zweiten Arguments hinter dem Operator zurückgegeben. Mit dem Ausdruck wird also überprüft, ob name einen brauchbaren Wert besitzt, welcher der Eigenschaft this.name zugewiesen werden kann. Falls ja, bekommt this.name diesen Wert. Andernfalls wird this.name ein leerer String zugewiesen. In diesem Kapitel wird auf dieses Konstrukt zurückgegriffen, um die Code-Beispiele knapp und übersichtlich zu halten.

Hinweis: Es kann passieren, dass dieses Konstrukt nicht wie vorgesehen funktioniert, wenn die Konstruktorfunktion mit Argumenten aufgerufen wird, die zu false evaluieren (wie z. B. 0 (Null) und der leere String (""). In diesem Fall wird der Default-Wert gewählt.

Mit diesen Definitionen kann man bei der Erstellung von Instanzen eines Objekts Werte für die lokalen Eigenschaften festlegen. Wie im Bild 8.5 bereits gezeigt, kann man die folgende Anweisung einsetzen, um eine neue Engineer-Instanz zu erzeugen:

var jane = new Engineer("belau");

Die Eigenschaften von Jane sind sodann:

jane.name == "";
jane.dept == "engineering";
jane.projects == [];
jane.machine == "belau"

Mit diesen Definitionen kann den vererbten Eigenschaften wie z. B. name kein Anfangswert zugewiesen werden; möchte man dies tun, so muss der Konstruktorfunktion weiterer Code hinzugefügt werden.

Bisher haben wir mit der Konstruktorfunktion ein generisches Objekt erstellt und dann lokale Eigenschaften und Werte zugewiesen. Ebenso kann man auch die Konstruktorfunktion eines in der Prototypkette höherliegenden Objekts aufrufen, um dem Objekt die entsprechenden Eigenschaften hinzuzufügen. Das folgende Bild zeigt diese neuen Definitionen.


Bild 8.6: Festlegen von Eigenschaften über den Konstruktor, Teil 2

Hier die neue Definition für die Engineer-Konstruktorfunktion:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}

Angenommen man erstellt nun ein Engineer-Objekt wie folgt:

var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

Dann führt JavaScript diese Schritte aus:

  1. Der new Operator erstellt ein generisches Objekt und setzt die __proto__-Eigenschaft auf Engineer.prototype.
  2. Der new-Operator übergibt das neue Obejekt an den Engineer-Konstruktor als Wert für this.
  3. Der Konstruktor erstellt eine neue Eigenschaft base für dieses Objekt und weist den Wert des WorkerBee-Konstruktors der Eigenschaft base zu. Dieser Vorgang macht den WorkerBee-Konstruktor zu einer Methode des Engineer-Objekts. Der Name der Eigenschaft base ist nicht speziell. Es kann jeder beliebige Bezeichner verwendet werden.
  4. Der Konstruktor ruft die Methode base auf und übergibt als Argumente zwei der Argumente, die an die Konstruktorfunktion übergeben wurden ("Doe, Jane" und ["navigator", "javascript"]) und außerdem noch den String "engineering". Da explizit der Wert "engineering" im Konstruktor angegeben wurde, erhalten alle  Engineer-Objekte denselben Wert für die vererbte Eigenschaft dept. Mit diesem Wert wird der von Employee geerbten Wert überschrieben.

  5. Da base eine Methode von Engineer ist, bindet JavaScript das this-Schlüsselwort innerhalb des Aufrufs von base an das im ersten Schritt erstellte Objekt. Darum übergibt die WorkerBee-Funktion im Gegenzug die Argumente "Doe, Jane" und "engineering" an die Employee-Konstruktorfunktion. Bei der Rückgabe der Employee Konstruktorfunktion, benutzt die WorkerBee-Function das verbleibende Argument für das Setzen der projects Eigenschaft.
  6. Vor dem Ende der base Methode initialisiert der Engineer-Konstruktor die Eigenschaft machine zu "belau".
  7. Zum Schluss wird das neue Engineer-Objekt der Variablen jane zugewiesen.

Es scheint so, als hätte man mit dem Aufruf des WorkerBee-Konstruktors aus dem Engineer-Konstruktor heraus die Vererbung für das Engineering-Objekte initiiert. Das ist jedoch nicht der Fall. Der Aufruf des WorkerBee-Konstuktors stellt sicher, dass ein Engineering-Objekt mit allen Eigenschaften erstellt wird, welche durch die aufgerufene Konstruktorfunktionen hinzugefügt wurden. Fügt man den Prototypen von Employee oder WorkerBee später Eigenschaften hinzu, so werden diese nicht vom Engineer-Objekt geerbt. Hat man z. B. die folgenden Anweisungen:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

Dann erbt das Objekt jane die Eigenschaft specialty nicht. Der Prototype muss explizit zugewiesen werden, um dynamische Vererbung umzusetzen. Verändert man den Code wie folgt:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

Dann bekommt die Eigenschaft specialty des jane-Objekts den Wert "none".

Eine weitere Möglichkeit zur Umsetzung von Vererbung ist der Einsatz der Methoden call() / apply(). Der folgende Code ist äquivalent:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
function Engineer (name, projs, mach) {
  WorkerBee.call(this, name, "engineering", projs);
  this.machine = mach || "";
}

Die Verwendung der Methode call() ermöglicht saubereren Code, weil das base-Objekt dabei nicht mehr gebraucht wird.

Vererbung von Eigenschaften (Fortsetzung)

Die vorhergehenden Abschnitte beschreiben, wie durch JavaScript-Konstruktoren und Prototypen eine Vererbungshierarchie aufgebaut werden kann. Dieser Abschnitt beschreibt die Feinheiten des Vererbungskonzepts.

Lokale und vererbte Werte

Beim Zugriff auf die Eigenschaft eines Objekts führt JavaScript nacheinander diese Schritte aus, wie bereits weiter oben beschreiben:

  1. Überprüfung, ob der Wert lokal existiert und wenn ja, Rückgabe des Werts.
  2. Wenn kein lokaler Wert existiert, Überprüfung der Prototypekette (über die Eigenschaft __proto__).
  3. Wenn ein Objekt in der Prototypekette einen Wert für die Eigenschaft besitzt, Rückgabe dieses Werts.
  4. Wurde die Eigenschaft nicht gefunden, besitzt das Objekt keine solche Eigenschaft.

Das Ergebnis dieses Ablaufs hängt von den Definitionen ab. Kommen wir noch einmal auf das Beispiel vom Anfang des Kapitels zurück:

function Employee () {
  this.name = "";
  this.dept = "general";
}

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

Erstellt man nach diesen Definitionen z. B. das Objekt amy als Instanz von WorkerBee mit der folgenden Anweisung:

var amy = new WorkerBee;

Dann besitzt amy nur die lokale Eigenschaft projects. Die Werte der Eigenschaften name und dept sind nicht lokal zu amy und werden daher über die Prototypekette bzw. die Eigenschaft __proto__ abgefragt. Also besitzt amy diese Eigenschaften und Werte:

amy.name == "";
amy.dept == "general";
amy.projects == [];

Ändert man nun die Eigenschaft name des Prototype, der mit Employee verknüpft ist:

Employee.prototype.name = "Unknown"

Dann würde man vielleicht zunächst annehmen, dass der neue Wert bis nach unten für alle Instanzen von Employee übernommen wird. Das ist jedoch nicht der Fall.

Bei jeder Erzeugung einer Instanz des Employee-Objekts bekommt diese Instanz einen lokalen Wert (einen leeren String) für die Eigenschaft name zugewiesen. Wenn man also den WorkerBee-Prototype festlegt, indem man ein neues Employee-Objekt erstellt, besitzt Workerbee.prototype eine Eigenschaft name mit einem lokalen Wert. Beim Zugriff auf die Eigenschaft findet JavaScript dann den lokalen Wert des Prototype WorkerBee.prototype und sucht die Prototypekette nicht weiter bis zu Employee.prototype ab.

Möchte man den Wert einer Objekt-Eigenschaft während der Laufzeit ändern und der neue Wert soll auch für alle erbenden Objekte gesetzt werden, so darf man diesen nicht in der Konstruktorfunktion zuweisen. Stattdessen weist man dem mit dem Objekt verknüpften Prototype den Wert zu.

Ändert man vorherigen Code z. B. wie folgt:

function Employee () {
  this.dept = "general";
}
Employee.prototype.name = "";

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

var amy = new WorkerBee;

Employee.prototype.name = "Unknown";

Dann besitzt die Eigenschaft name von amy den Wert "Unknown".

Wie diese Beispiele zeigen, sollte man also Werte dem Prototype des Konstruktors zuweisen und nicht in der Konstruktorfunktion, falls man Default-Werte für Objekteigenschaften setzen und diese während der Laufzeit ändern möchte.

Festlegen von Instanzen-Beziehungen

Beim Zugriff auf Eigenschaften wird zuerst überprüft, ob das Objekt selbst eine entsprechende Eigenschaft besitzt. Wird die Eigenschaft lokal nicht gefunden, sucht JavaScript in der speziellen Objekteigenschaft __proto__. Dieser Ablauf wird rekursiv fortgeführt, solange bis eine Eigenschaft gefunden oder festgestellt wurde, dass keines der Objekte der Prototypekette eine solche Eigenschaft besitzt.

Die spezielle Eigenschaft __proto__ wird bei der Erzeugung eines Objekts gesetzt. Der Wert dieser Eigenschaft ist dann die prototype-Eigenschaft des Konstruktors. Demzufolge wird mit dem Ausdruck new Foo() ein Objekt mit __proto__ == Foo.prototype erstellt. Änderungen der Eigenschaften von Foo.prototype werden für alle Objekte übernommen, die mit new Foo() erzeugt wurden.

Jedes Objekt besitzt eine __proto__-Eigenschaft (mit Ausnahme von Object) und jede Funktion hat eine prototype-Objekteigenschaft. Objekte können also durch die Prototype-Vererbung mit anderen Objekten verwandt sein. Diese Verwandtschaft kann man überprüfen, indem man die __proto__-Eigenschaften mit dem prototype-Objekt einer Funktion vergleicht. Der Operator instanceof vergleicht ein Objekt mit einer Funktion und liefert true zurück, wenn das Objekt vom Prototype der Funktion erbt.

Hier ein Beispiel:

var f = new Foo();
var isTrue = (f instanceof Foo);

Hier noch ein weiteres Beispiel: Angenommen man hat dieselben Definitionen wie unter Vererbung von Eigenschaften und erstellt ein Engineer-Objekt wie folgt:

var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");

Dann evaluieren die folgenen Anweisungen alle zu true:

chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;

Und man könnte z. B. eine Funktion instanceOf wie folgt schreiben:

function instanceOf(object, constructor) {
   while (object != null) {
      if (object == constructor.prototype)
         return true;
      if (typeof object == 'xml') {
        return constructor.prototype == XML.prototype;
      }
      object = object.__proto__;
   }
   return false;
}
Achtung: Die Implementierung dieser Funktion überprüft den Typ des Objekts gegen "xml", um eine Eigenheit von neueren JavaScript-Versionen bei der Repräsentation von XML-Objekten zu beachten. Siehe bug 634150 für weitere Informationen.

Bei Verwendung der instanceOf-Funktion von oben evaluieren diese Ausdrücke zu true:

instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)

Der folgende Ausdruck evaluiert indessen zu false:

instanceOf (chris, SalesPerson)

Globale Informationen in Konstruktoren

Bei der Erstellung von Konstruktoren ist Vorsicht geboten, wenn man globale Werte im Konstruktor verändert. Möchte man z. B. für jedes Empoyee-Objekt eine eindeutige ID generieren und automatisch dem Objekt zuweisen, so könnte man Employee wie folgt definieren:

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

Mit dieser Definition weist der Konstruktor bei der Erstellung eines Empoyee-Objekts diesem eine ID zu und inkrementiert anschließend die globale Zählervariable idCounter. Wenn die nächsten Anweisungen wie folgt gemacht werden, bekommt  victoria.id den Wert 1 und harry.id den Wert 2:

var victoria = new Employee("Pigbert, Victoria", "pubs");
var harry = new Employee("Tschopik, Harry", "sales");

Auf den ersten Blick mag diese Vorgensweise unproblematisch erscheinen. Man sollte jedoch bedenken, dass idCounter bei jeder Erstellung eines Employee-Objekts inkrementiert wird. Hätte man z. B. unter Verwendung dieser Definitionen die komplette in diesem Kaptitel beschriebene Vererbungshierarchie erstellt, dann würde der Employee-Konstuktor bei jeder Zuweisung eines Prototype aufgerufen.

Angenommen man hat den folgenden Code:

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

function Manager (name, dept, reports) {...}
Manager.prototype = new Employee;

function WorkerBee (name, dept, projs) {...}
WorkerBee.prototype = new Employee;

function Engineer (name, projs, mach) {...}
Engineer.prototype = new WorkerBee;

function SalesPerson (name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;

var mac = new Engineer("Wood, Mac");

Weiter angenommen, die Funktionen der übrigen Objekte, deren Definitionen hier nicht gezeigt wurden, würden die base-Eigenschaft besitzen und den Konstruktor des darüberliegenden Objekts in der Prototypekette aufrufen. Dann bekäme mac.id bei der Erstellung von mac den Wert 5.

Ob die zusätzliche Inkrementierung erforderlich ist oder nicht, hängt von der Applikation ab. Damit die Zählervariable einen exakten Wert bekommt, könnte man die Definition wie folgt ändern:

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   if (name)
      this.id = idCounter++;
}

Bei der Erstellung einer Employee-Instanz, die als Prototype dient, übergibt man einfach keine Argumente an den Konstruktor. Beim Aufruf des Konstruktors wird dann id kein Wert zugewiesen und die Zählervariable nicht erhöht. Damit eine Employee-Instanz eine ID zugewiesen bekommt, muss name als Argument übergeben werden. Bei diesem Beispiel hätte dann mac.id den Wert 1.

Keine mehrfache Vererbung

Manche objektorientierte Programmiersprachen erlauben mehrfache Vererbung. Das bedeutet, dass Objekte die Eigenschaften und Werte von nicht verwandten Elternobjekten erben können. JavaScript unterstützt diese mehrfache Vererbung nicht.

Die Vererbung von Eigenschaftswerten erfolgt zur Laufzeit, indem JavaScript die Prototypekette nach dem Wert absucht. Da ein Objekt nur mit einem einzelnen Prototypen verknüpft werden kann, ist es nicht möglich, dass ein Objekt dynamisch von mehreren Prototypeketten erbt.

Bei JavaScript kann eine Konstruktorfunktion mehrere andere Konstruktorfunktionen aufrufen. Damit lässt sich etwas ähnliches wie mehrfache Vererbung nachbilden. Dies ließe sich z. B. wie in folgendem Beispiel umsetzen:

function Hobbyist (hobby) {
   this.hobby = hobby || "scuba";
}

function Engineer (name, projs, mach, hobby) {
   this.base1 = WorkerBee;
   this.base1(name, "engineering", projs);
   this.base2 = Hobbyist;
   this.base2(hobby);
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;

var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")

Wird diese Definition so benutzt, wie im vorherigen Abschnitt gezeigt, dann besitzt das Objekt dennis diese Eigenschaften:

dennis.name == "Doe, Dennis"
dennis.dept == "engineering"
dennis.projects == ["collabra"]
dennis.machine == "hugo"
dennis.hobby == "scuba"

Die Eigenschaft hobby des dennis-Objekts bekommt also nicht den Wert des Hobbyist-Konstruktors zugewiesen. Und angenommen man fügt dem Prototype des Hobbyist-Konstruktors die Eigenschaft hinzu:

Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]

Dann erbt das Objekt dennis diese neue Eigenschaft nicht.