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