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 KlasseEmployee
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.
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 Eigenschaftenname
(mit einem leeren String als Anfangswert) unddept
(mit dem Anfangswert "general").Manager
basiert auf demEmployee
-Objekt und besitzt die zusätzliche Eigenschaftreports
. Der Anfangswert ist ein leeres Array, das für die Speicherung vonEmployee
-Objekten vorgesehen ist.WorkerBee
basiert ebenfalls auf demEmployee
-Objekt und besitzt die zusätzliche Eigenschaftprojects
(mit einem leeren Array als Anfangswert, das für die Speicherung von Stringwerten vorgesehen ist).SalesPerson
basiert auf dem ObjektWorkerBee
und besitzt die zusätzliche Eigenschaftquota
(mit100
als Anfangswert). Außerdem wird der Wert der Eigenschaftdept
mit dem Wert "sales" überschrieben, wodurch ersichtlich werden soll, dass Verkaufsleute in derselben Abteilung beschäftigt sind.Engineer
basiert auf dem ObjektWorkerBee
und besitzt die zusätzliche Eigenschaftmachine
(mit einem leeren String als Anfangswert). Der Wert der Eigenschaftdept
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.
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 |
---|---|
|
|
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 |
---|---|
|
|
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 |
---|---|
|
|
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.
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.
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 |
---|---|
|
|
|
|
|
|
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.
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:
- Der
new
Operator erstellt ein generisches Objekt und setzt die__proto__
-Eigenschaft aufEngineer.prototype
. - Der
new
-Operator übergibt das neue Obejekt an denEngineer
-Konstruktor als Wert fürthis
. - Der Konstruktor erstellt eine neue Eigenschaft
base
für dieses Objekt und weist den Wert desWorkerBee
-Konstruktors der Eigenschaftbase
zu. Dieser Vorgang macht denWorkerBee
-Konstruktor zu einer Methode desEngineer
-Objekts. Der Name der Eigenschaftbase
ist nicht speziell. Es kann jeder beliebige Bezeichner verwendet werden. -
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 alleEngineer
-Objekte denselben Wert für die vererbte Eigenschaftdept
. Mit diesem Wert wird der vonEmployee
geerbten Wert überschrieben. - Da
base
eine Methode vonEngineer
ist, bindet JavaScript das this-Schlüsselwort innerhalb des Aufrufs vonbase
an das im ersten Schritt erstellte Objekt. Darum übergibt dieWorkerBee
-Funktion im Gegenzug die Argumente"Doe, Jane"
und"engineering"
an dieEmployee
-Konstruktorfunktion. Bei der Rückgabe derEmployee
Konstruktorfunktion, benutzt dieWorkerBee
-Function das verbleibende Argument für das Setzen derprojects
Eigenschaft. - Vor dem Ende der
base
Methode initialisiert derEngineer
-Konstruktor die Eigenschaftmachine
zu"belau"
. - Zum Schluss wird das neue
Engineer
-Objekt der Variablenjane
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:
|
|
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:
- Überprüfung, ob der Wert lokal existiert und wenn ja, Rückgabe des Werts.
- Wenn kein lokaler Wert existiert, Überprüfung der Prototypekette (über die Eigenschaft
__proto__
). - Wenn ein Objekt in der Prototypekette einen Wert für die Eigenschaft besitzt, Rückgabe dieses Werts.
- 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__ ==
erstellt. Änderungen der Eigenschaften von Foo.prototype
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;
}
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.