Orlok: Die Vererbung

Beitrag lesen

Hallo

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

Dagegen beschützt dich der strict-Mode.

Ja, mich schon, aber nicht denjenigen, der vielleicht ohne Kenntnis dieses Features den Code aus meinen Beispielen übernimmt; Deshalb wollte ich es nicht unerwähnt lassen… ;-)

Obwohl ein entsprechender Hinweis auf den strict-Mode hier sicher nicht geschadet hätte – da hast du vollkommen recht! Eine kurze Erklärung für den interessierten Mitleser sei also nachgereicht:

Nehmen wir also einmal an, wir haben die folgende als Konstruktor bestimmte Funktion…

var Constructor = function ( ) {
  this.property = 'value';
};

…und würden sie ohne das Schlüsselwort new aufrufen…

Constructor( );

…dann würde this auf das globale Objekt, also window verweisen und property wäre eine globale Variable:

console.log(property); // value

console.log(!!window.property); // true

Würden wir aber statt dessen zum Beispiel schreiben…

var Constructor = function ( ) {
  'use strict';
  this.property = 'value';
};

…dann würde this beim Funktionsaufruf ohne new nicht mehr auf window zeigen, sondern mit undefined initialisiert werden, wobei die Eigenschaftszuweisung dann einen type error produziert, wodurch das Programm abgebrochen und der Fehler beim Aufruf der Funktion sofort offensichtlich wird:

Constructor( ); // type error: this is undefined

Use strict!

Das also dazu. ;-)


Im Folgenden mein etwas gekürztes Ursprungsbeispiel:


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

Kaefer.prototype.gesundheit = 0;

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

var gregor = new Kaefer('Gregor');

gregor.bewerfen( );

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

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.

Interessant in dem Zusammenhang ist, dass die Methode bewerfen bei ihrem ersten Aufruf eine neue Eigenschaft direkt auf der Instanz erzeugt, und nicht die Eigenschaft des Prototyps verändert […]

Vielen Dank für den Hinweis! Das ist mir im Eifer des Gefechts gar nicht aufgefallen, aber das wäre definitiv einer Erwähnung wert gewesen, zumal das nicht nur „interessant“ ist, sondern eine fundamentale Voraussetzung dafür, dass dieses Prinzip der Vererbung überhaupt zuverlässig und vor allem praktikabel funktioniert, denn wie du treffend bemerkt hast:

Eine unmittelbare Auswirkung ist, dass die bewerfen-Methode die Zustände anderer Kaefer-Instanzen unverändert lässt.

Dadurch, dass bei der Zuweisung eines Wertes zu einer geerbten Eigenschaft – in diesem Fall also der Eigenschaft gesundheit von Kaefer.prototype – automatisch eine neue eigene Eigenschaft der Instanz – hier also gregor – erzeugt wird, wird sichergestellt, das sich Veränderungen nur lokal auswirken, sprich, wäre dem nicht so, müsste man immer die Herkunft einer Eigenschaft berücksichtigen und im Zweifel explizit eine neue eigene Eigenschaft definieren.

Dazu gleich noch etwas mehr.


Aber zunächst noch eine Randbemerkung zu deinem Code-Beispiel:

var gregor = new Kaefer('gregor');
console.assert(gregor.hasOwnProperty('gesundheit') === false, 'Gesundheit ist eine direkte Eigenschaft von gregor');
gregor.bewerfen();
console.assert(gregor.hasOwnProperty('gesundheit') === true, 'Gesundheit ist keine direkte Eigenschaft von gregor');

Die Meldungen sind etwas konfus, weil sie an der Stelle Fehlermeldungen darstellen und keine Erläuterungen. Sie beschreiben also den negativen Fall, dass die Assertions fehlschlagen, nicht den positiven Fall (hier fallen beide Assertions positiv aus)

Zur asynchronen Überwachung von Objekten gibt es eine bessere Methode als console.assert, die aber leider noch nicht offizieller ECMAScript-Standard ist und die auch noch nicht browserübergreifend funktioniert, nämlich Object.observe; Sowie zur Beendigung der Überwachung, die Schwestermethode Object.unobserve; Siehe hierzu den entsprechenden Vorschlag.

Die Syntax: Object.observe( object, callback [, array] );

Das erste Argument ist das Objekt, welches überwacht werden soll, das zweite Argument ist eine Callback-Funktion, die bei eingetretener Veränderung aufgerufen wird, und das optionale dritte Argument ist ein Array, welches die Bezeichner der zu berücksichtigenden Veränderungen als Strings enthält.

Der Callback-Funktion wird bei ihrem Aufruf als Parameter ein Array übergeben, welches für jede erfolgte Veränderung ein Objekt enthält, dem wiederum sachdienliche Informationen entnommen werden können.

Bis dato beschränkt auf Chrome und Opera, könnte man für unseren Fall also auch schreiben:

var gregor = new Kaefer('Gregor');

Object.observe(gregor, function (changes) {
  console.log('Folgende Eigenschaft wurde zum Objekt hinzugefügt: ' + changes[0].name);
}, ['add']);

gregor.bewerfen( );

Hier übergeben wir also zunächst das Instanzobjekt gregor an die Methode und bestimmen dann, dass der Name der Veränderten Eigenschaft in die Konsole geschrieben wird, indem wir auf die name-Eigenschaft des ersten Objektes im Array zugreifen, in welcher der Bezeichner der betroffenen Eigenschaft hinterlegt ist.

Schließlich bestimmen wir mit der Übergabe des Arrays ['add'], dass unsere Callback-Funktion nur aufgerufen werden soll, wenn eine Eigenschaft hinzugefügt wurde. Was dann mit dem Aufruf der Methode bewerfen auch passiert…

// Folgende Eigenschaft wurde hinzugefügt: gesundheit

Ich halte das jedenfalls für ein außerordentlich nützliches Feature und hoffe, dass es bald auch von Mozilla und Microsoft implementiert, sowie in die nächste Version des ECMAScript-Standards aufgenommen wird.


Aber gut, zurück zum eigentlichen Thema! ;-)

Wir hatten festgestellt, dass um die Invarianz vererbter Eigenschaften sicherzustellen, bei der Zuweisung (assignment) auf der Instanz automatisch eine neue eigene Eigenschaft angelegt wird.

Aber dieser Grundsatz gilt nicht immer! – Beispiel:

Object.freeze(Kaefer.prototype);

gregor.bewerfen( );

console.log(gregor.gesundheit); // 0

console.log(gregor.hasOwnProperty('gesundheit')); // false

Hier haben wir auf Kaefer.prototype die Methode Object.freeze angewendet, und der Aufruf der vererbten Methode bewerfen bewirkt überhaupt nichts:

Weder verändert sich der Wert der ebenfalls geerbten Eigenschaft gesundheit, noch wird auf der Instanz eine neue eigene Eigenschaft mit dieser Bezeichnung angelegt!

Um nun zu verstehen, was hier passiert, müssen wir erst einmal wissen, dass Objekteigenschaften in JavaScript selbst wiederum über mehrere interne Eigenschaften verfügen (um Verwechslungen zu vermeiden im Folgenden Attribute genannt), welche in einem assoziierten Objekt hinterlegt sind, welches als property descriptor bezeichnet wird:

var farbe = Object.getOwnPropertyDescriptor(gregor, 'farbe');

Mit der Methode Object.getOwnPropertyDescriptor können wir für jede eigene Objekteigenschaft die dazugehörigen Attribute auslesen, indem wir das Objekt sowie den Bezeichner der Eigenschaft als Argumente übergeben.

Der Rückgabewert der Methode ist dann der property descriptor, welcher in unserem Beispiel für gregor.farbe so aussehen würde:

{ value : 'braun', writable : true, enumerable : true, configurable : true } // farbe

Die Eigenschaften des property descriptors repräsentieren hier die dazugehörigen internen Eigenschaften [[Value]], [[Writable]], [[Enumerable]] und [[Configurable]], wobei in diesem Zusammenhang nur das Attribut writable von Interesse ist, welches darüber bestimmt, ob der Wert der Eigenschaft, welcher in value hinterlegt ist, verändert werden darf.

Schauen wir uns einmal die Attribute einer Eigenschaft von Kaefer.prototype an, auf welche wir die Methode Object.freeze angewendet haben…

var gesundheit = Object.getOwnPropertyDescriptor(Kaefer.prototype, 'gesundheit');

{ value: 0, writable: false, enumerable: true, configurable: false } // gesundheit

…und wir sehen, dass die Attribute writable und configurable jeweils auf false gesetzt wurden.

Dadurch, dass wir mit (der Holzhammermethode) Object.freeze den Wert der Eigenschaft gesundheit auf read-only gesetzt haben, haben wir nicht nur bewirkt, dass die Originaleigenschaft auf Kaefer.prototype nicht mehr verändert werden kann, sondern wir haben außerdem dafür gesorgt, dass diese Eigenschaft auch nicht durch assignment, also durch bloße Zuweisung auf einer Instanz überschrieben wird.

Nebenbei sei übrigens noch erwähnt, dass im strict-Mode, bei dem Versuch eine Eigenschaft welche auf read-only gesetzt ist zu verändern, eine Fehlermeldung geworfen wird, sonst nicht!

Jedenfalls sieht es anders aus, wenn wir die entsprechende Eigenschaft nicht durch Zuweisung, sondern durch Definition hinzufügen, also vor dem Aufruf unserer Methode bewerfen zum Beispiel folgendes schreiben:

Object.defineProperty(gregor, 'gesundheit', {
  value : 0,
  writable : true
});

gregor.bewerfen( );

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

console.log(gregor.hasOwnProperty('gesundheit')); // true

Der Wert des Attributes writable einer vererbten Eigenschaft bestimmt also nicht nur, ob der Wert der Eigenschaft auf dem Objekt, dessen eigene Eigenschaft sie ist, überschrieben werden darf, sondern ebenso, ob bei einem Instanzobjekt durch assignment eine neue eigene Eigenschaft angelegt wird oder nicht.

Wird eine Eigenschaft als read-only initialisiert, dann bleibt sie also über die gesamte Prototypenkette hinweg konstant, und kann auch auf einer Instanz nur durch explizite Definition überschrieben werden.

Gruß,

Orlok