Orlok: Die Vererbung

Beitrag lesen

Hallo

Das heißt, mit var obj = { } erzeugst du eine Instanz von Object.prototype […]

Das hast du unglücklich formuliert […]

Das habe ich unpräzise formuliert. Eigentlich sogar falsch. Aber nicht „unglücklich“! ;-)

Instanzen werden in JavaScript von Konstruktor-Funktionen gebildet.

So ist es. Aber bloß auf den Konstruktor zu verweisen würde der Sache auch nicht wirklich gerecht, oder?

Nehmen wir also einmal an, wir würden eines Morgens aus unruhigen Träumen erwachen und die folgende Funktion notieren…

var Kaefer = function (name) {
  this.name = name;
  this.typ = 'Ungeziefer';
  this.farbe = 'braun';
};

…und diese sogleich als Konstruktor aufrufen:

var gregor = new Kaefer('Gregor');

Dann wäre es absolut nachvollziehbar zu sagen, dass gregor ein Kaefer ist und es sich hierbei also um eine Instanz des Konstruktors handelt:

var constructor = gregor.constructor; // function Kaefer()

oder

var check = gregor instanceof Kaefer; // true

Indem wir Kaefer als Konstruktor aufrufen, erzeugen wir ein neues leeres Object, welches wir innerhalb der Konstruktorfunktion über this referenzieren können. Das heißt, wenn wir direkt im Konstruktor Eigenschaften und Methoden an die Variable this knüpfen, dann sind dies, sofern wir die Funktion auch tatsächlich als Konstruktor aufrufen, Eigenschaften und Methoden des erzeugten Objektes selbst:

var ungeziefer = gregor.hasOwnProperty('typ'); // true

Würden wir hingegen das Schlüsselwort new bei unserem Funktionsaufruf vergessen, dann hätten wir unter Anderem ein paar neue globale Variablen produziert…

Davon aber einmal abgesehen, hätten wir hier in diesem Fall auch gleich folgendes schreiben können:

var kaefer = function (name) {
  return {
    name : name,
    typ : 'Ungeziefer',
    farbe : 'braun'
  };
};

var gregor = kaefer('Gregor');

var ungeziefer = gregor.hasOwnPrototype('typ'); // true

Wie dem auch sei, bezogen auf unseren Kaefer-Konstruktor besteht soweit jedenfalls kein Zweifel an dessen Rolle als "Identitätsstifter" hinsichtlich der von ihm erzeugten Objekte.

Aber als Funktionsobjekt besitzt unser Konstruktor ja auch noch eine Eigenschaft namens prototype, bei der es sich ebenso um ein (beinahe) leeres Object handelt, welchem wir ebenfalls Eigenschaften und Methoden zuweisen können:

var Kaefer = function (name) {
  this.name = name;
  this.typ = 'Ungeziefer';
  this.farbe = 'braun';
};

Kaefer.prototype.beruf = false;

Kaefer.prototype.gesundheit = 0;

Kaefer.prototype.bewerfen = function ( ) {
  this.gesundheit -= Math.ceil(Math.random( ) * 10);
};

Wenn wir nun also Kaefer als Konstruktor aufrufen, dann verfügt gregor nicht nur über die direkt in der Funktion deklarierten Eigenschaften, sondern ebenso über die Eigenschaften und Methoden, welche von uns in Kaefer.prototype hinterlegt wurden:

var gregor = new Kaefer('Gregor');

gregor.bewerfen( );
gregor.bewerfen( );

var status = gregor.gesundheit; // z.B. -7  :-(

Zwischen den direkt im Konstruktor angelegten Objekteigenschaften und denjenigen, welche wir im Prototypobjekt des Konstruktors spezifiziert haben, besteht allerdings ein Unterschied:

var farbe = gregor.hasOwnProperty('farbe'); // true

var beruf = gregor.hasOwnProperty('beruf'); // false

Bei den Eigenschaften, welche wir für das Prototypobjekt der Konstruktorfunktion angelegt hatten, handelt es sich also nicht um eigene Eigenschaften unseres Instanzobjektes, sondern um geerbte.

Jedes Objekt in JavaScript hat eine interne Eigenschaft [[Prototype]], welche standardmäßig auf das prototype-Objekt seines Konstruktors verweist, und dem jeweiligen Objekt so dessen Eigenschaften und Methoden zur Verfügung stellt:

var prototyp = Object.getPrototypeOf(gregor);

Oder auch…

var prototyp = gregor.__proto__;

…wobei ich mir nicht sicher bin, ob letztere Schreibweise zum Zugriff auf die interne [[Prototype]] Eigenschaft nun überholt ist oder nicht, aber die Einordnung in der Spezifikation unter legacy features spricht dafür, am besten grundsätzlich die erste Variante zu verwenden. ;-)

Jedenfalls hat auch der Prototyp eines Objektes selbst wiederum einen Prototyp, so dass eine Prototypenkette entsteht, die bei Object.prototype und schließlich bei null endet.

Als plain object ist der Prototyp von Kaefer.prototype, in dem wir zusätzliche Eigenschaften hinterlegt haben, Object.prototype, aber das kann man natürlich auch ändern, weshalb wir zunächst ein neues Objekt erstellen…

var properties = {
  nachname : 'Samsa',

  "familie" : ['Vater', 'Mutter', 'Grete'],

  beruf : 'Handelsreisender',

  hobbys : ['Zeitunglesen', 'Laubsägearbeiten', 'aus dem Fenster gucken'],

  typ : 'Mensch'
};

…und welches wir dann als Prototypen von Kaefer.prototype festlegen:

Object.setPrototypeOf(Kaefer.prototype, properties);

// Oder, wenn auch weniger empfehlenswert:

Kaefer.prototype.__proto__ = properties;

Nachdem wir dies also vollbracht haben, verfügt gregor nun über eine ganze Reihe an Eigenschaften, von denen lediglich name, typ und farbe eigene Eigenschaften sind, welche in der Konstruktorfunktion spezifiziert wurden, während der gesamte Rest über die Prototypenkette geerbt wurde.


Dabei soll allerdings nicht unerwähnt bleiben, dass "nachträgliche" Eingriffe in die prototype chain teure Operationen sind, die hier nur aus dramaturgischen Gründen vorgeführt wurden: In der Regel sollte die Kette unter Verwendung von Object.create( ) von unten aufgebaut werden, indem man der genannten Methode ein geeignetes Prototypobjekt als Argument übergibt, welches dann dem zurückgegebenen neuen Objekt als Wert der [[Prototype]] Eigenschaft dient. Aber das nur nebenbei bemerkt.


Jedenfalls sollten wir einen Blick auf ein paar Eigenschaften von gregor werfen:

var beruf = gregor.beruf; // false

Warum false und nicht 'Handelsreisender', wie wir es gerade bestimmt haben?

Weil – ähnlich dem Scope von Variablen – nur dann in einem assoziierten Prototypobjekt nach einer entsprechenden Eigenschaft gesucht wird, wenn unter dem selben Bezeichner keine eigene Eigenschaft vorhanden ist.

Das heißt, da die Eigenschaft beruf sowohl in Kaefer.prototype als auch in dessen Prototypobjekt properties hinterlegt ist, wird aus Sicht von gregor auf die nächste Eigenschaft in der Kette, also die in Kaefer.prototype bestimmte Eigenschaft beruf mit dem Wert false zugegriffen.

Wenn wir diese Eigenschaft löschen…

delete Kaefer.prototype.beruf;

…wird beim nächsten Prototypen in der Kette nachgefragt und voilà…

var beruf = gregor.beruf; // Handelsreisender

Und selbstverständlich handelt es sich bei gregor nicht um 'Ungeziefer':

delete gregor.typ;

var check = gregor.typ; // 'Mensch' ;-)

Tja, und wenn wir uns nun anschauen, was gregor an eigenen Eigenschaften geblieben ist, dann ist das nicht besonders viel, im Vergleich zu dem, was ihn sonst noch so ausmacht…

var names = Object.getOwnPropertyNames(gregor); // ['name', 'farbe']

…aber dennoch gilt nach wie vor:

console.log(gregor instanceof Kaefer); // true :-/

Also, was ist die Moral von der Geschichte?

Es war kein Traum! Obwohl fast alle Eigenschaften von gregor aus der Prototypenkette geerbt wurden, ist er nach wie vor ein Kaefer, also eine Instanz seines Konstruktors

Aber beschreibt das die Herkunft und Identität unseres Objektes wirklich zufriedenstellend?

Wenn ich ein Array erzeuge, var arr = [ ], ist dann wirklich der Konstruktor Array( ) von Interesse oder nicht doch eher Array.prototype, die Quelle der Methoden, die ich auf meinem Array anweden will?

Vielleicht ist der Begriff „Instanz“ im Zusammenhang mit der prototypischen Vererbung in JavaScript ganz einfach keine all zu treffende Bezeichnung. ;-)

Wie auch immer…

Gruß,

Orlok