Orlok: Tutorials für JavaScript Patterns gesucht

Beitrag lesen

Hallo MB

Ich hab mal unter anderem auf YouTube gesucht und gefunden. Das ist leider auf Englisch und beinhaltet viele neue Funktionen z.B. prototype oder call. Diese sind glaube ich in der aktuellen JS Version aufgelistet und gabs früher noch nicht.

Weder prototype noch call sind in irgendeiner Form neu, und bei prototype handelt es sich zumindest auf Sprachebene auch nicht um eine Funktion, jedenfalls in der Regel nicht. Eine Ausnahme von der Regel ist allerdings Function.prototype, dessen eigene Methode call ist. Aber das ist in diesem Zusammenhang kaum relevant.

Die Eigenschaft prototype von Funktionsobjekten dient der Implementierung prototypischer Vererbung und ihr Wert ist üblicherweise ein gewöhnliches Objekt; Wird eine Funktion als Konstruktor aufgerufen, also entweder durch den Operator new oder unter Verwendung der Methode Reflect.construct, dann wird von der Funktion automatisch ein neues gewöhnliches Objekt erzeugt und zurückgegeben.

Dieses erzeugte Objekt wiederum hat eine Verbindung zu der Eigenschaft prototype des Konstruktors, das heißt, der Prototyp des erzeugten Objektes ist der Wert dieser Eigenschaft. Ist nun, wie dies standardmäßig der Fall ist, als Wert der Eigenschaft prototype ein Objekt hinterlegt, dann können dessen Eigenschaften und Methoden auch über das neu erzeugte Instanzobjekt referenziert werden; Sie werden also an dieses Objekt vererbt.

Sehen wir uns das Ganze aber mal an einem Beispiel etwas genauer an, wobei wir zunächst eine Funktion definieren, die wir dann später als Konstruktor aufrufen werden; Weil abgesehen von Pfeilfunktionen prinzipiell alle selbstdefinierten Funktionen als Konstruktor verwendet werden können, spielt die Art der Definition dabei keine Rolle.

Das heißt, es ist unerheblich, ob die Funktion wie im folgenden Beispiel deklariert wird, oder ob sie als Ausdruck notiert und das Funktionsobjekt einer Variable, Konstante oder Objekteigenschaft als Wert zugewiesen wird. Zu beachten ist hier ersteinmal nur die allgemeine Konvention, den ersten Buchstaben des Funktionsbezeichners groß zu schreiben, wodurch die Absicht zum Ausdruck gebracht wird, diese Funktion als Konstruktor zu verwenden.

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

Wird eine Funktion als Konstruktor aufgerufen, dann wird wie gesehen ein neues Objekt erzeugt. Dieses Objekt kann innerhalb des Funktionskörpers über die Kontextvariable this angesprochen werden, sprich, die Funktion wird beim Aufruf als Konstruktor im Kontext des Instanzobjektes ausgeführt.

Entsprechend können wie in dem Beispiel oben bereits bei der Ausführung des Konstruktors eigene Eigenschaften und Methoden auf dem erzeugten Objekt definiert werden. Hier also eine Eigenschaft mit dem Namen property, welcher der der Wert 'value' zugewiesen wird. Jede von diesem Konstruktor erzeugte Instanz besitzt demnach also selbst eine solche Eigenschaft.

Zu beachten ist hier darüber hinaus die Anweisung 'use strict', die dafür sorgt, dass die Funktion Constructor im Strict Mode ausgeführt wird. Dies ist eine Vorsichtsmaßnahme um zu verhindern, dass versehentlich globale Variablen erzeugt werden, wenn die Funktion nicht als Konstruktor aufgerufen wird.

Denn im normalen Ausführungsmodus verweist this bei einem gewöhnlichen Funktionsaufruf auf das globale Objekt, also üblicherweise window, dessen Eigenschaften in der Regel gleichzeitig globale Variablen sind. Würde Constructor also im normalen Ausführungsmodus aufgerufen und dabei das Schlüsselwort new vergessen werden, dann würde eine neue globale Variable mit dem Bezeichner property angelegt.

Im Strict Mode ist der Wert von this bei einem normalen Funktionsaufruf hingegen nicht window, sondern der primitive Wert undefined. Das heißt, in dem Fall, dass beim Aufruf das Schlüsselwort new vergessen wird, würde keine globale Variable angelegt, sondern die Zuweisung würde einen Fehler produzieren.

function Constructor ( ) {
  if (!new.target) {
    throw new TypeError('Constructor called without new');
  }
  this.property = 'value';
}

Zu diesem Zweck könnte man übrigens auch den Operator new.target verwenden, der im Falle des Aufrufs als Konstruktor auf die aufgerufene Funktion verweist, hier also Constructor, sonst jedoch den Wert undefined zurückgibt. Hierbei handelt es sich allerdings in der Tat um ein relativ neues Feature, dass gegebenenfalls noch nicht von allen relevanten Ausführungsumgebungen unterstützt wird.

console.log(typeof Constructor.prototype); // object

Jedenfalls besitzt unser Konstruktor wie alle gewöhnlichen Funktionen eine Eigenschaft mit dem Namen prototype, in der ein planes Objekt hinterlegt ist. Dieses Objekt besitzt standardmäßig nur eine eigene Eigenschaft, nämlich die Eigenschaft constructor, welche eine Referenz auf das dazugehörige Funktionsobjekt enthält, hier also auf unsere Funktion Constructor.

const value = Constructor.prototype.constructor;

console.log(value === Constructor); // true

Nun definieren wir auf dem Objekt, welches in der Eigenschaft Constructor.prototype hinterlegt ist eine Methode, welche die Anweisung beinhaltet, den Wert der Eigenschaft property des Objektes zurückzugeben, in dessen Kontext die Methode ausgeführt wird.

Constructor.prototype.method = function ( ) {
  return this.property;
};

Dabei ist zu berücksichtigen, dass die Kontextvariable this im Falle eines Methodenaufrufs immer auf das Objekt zeigt, auf dem die Funktion aufgerufen wurde. Das heißt, wenn der Aufruf über einen Elementausdruck nach dem Schema object.method() erfolgt, dann ist der Wert von this innerhalb der Funktion immer das Objekt, über das die Funktion beim Aufruf referenziert wurde.

const instance = new Constructor;

console.log(typeof instance); // object

Nachdem wir nun also eine Konstruktorfunktion erstellt haben und auf dem Objekt, welches in der Eigenschaft prototype hinterlegt ist, eine Methode definiert haben, rufen wir die Funktion jetzt mittels des Operators new als Konstruktor auf, sodass ein neues Objekt erzeugt und zurückgegeben wird, das in der Konstante mit dem Bezeichner instance gespeichert wird.

Da der Aufruf von Constructor durch den Operator new erfolgt und wir hier keine Argumente an die Funktion übergeben, können wir uns die Notierung der runden Klammern nach dem Funktionsbezeichner sparen.

console.log(instance.hasOwnProperty('property')); // true

console.log(instance.property); // value

Unser auf diese Weise erzeugtes Instanzobjekt besitzt nun eine eigene Eigenschaft namens property deren Wert der String 'value' ist, da wir im Funktionskörper unseres Konstruktors die entsprechende Zuweisung vorgenommen haben. Aber das ist natürlich nicht alles.

console.log(instance.hasOwnProperty('method')); // false

const value = instance.method( );

console.log(value); // value

Denn wir haben auf dem Objekt Constructor.prototype ja noch eine Methode mit dem Namen method definiert. Diese Methode wird nun bei der Erzeugung des Instanzobjektes nicht auf dieses kopiert, weshalb die Überprüfung mittels hasOwnProperty, ob es sich um eine eigene Eigenschaft handelt, entsprechend das Ergebnis false ergibt; Aber sie kann dennoch auf der Instanz aufgerufen werden, da Constructor.prototype der Prototyp dieses Objektes ist.

Das heißt, bei dem Versuch, die Methode method auf instance zu referenzieren wird intern zunächst geprüft, ob das Objekt instance selbst über eine solche Methode verfügt. Was hier nicht der Fall ist. Da die Suche auf diesem Objekt also erfolglos bleibt, wird in einem zweiten Schritt nachgesehen, ob der Prototyp von instance über eine entsprechende Methode verfügt.

const proto = Object.getPrototypeOf(instance);

console.log(proto === Constructor.prototype); // true

Da der Prototyp von instance nun das Objekt Constructor.prototype ist und weil auf diesem Objekt eine Methode mit dem Namen method definiert ist, wird schließlich diese Methode referenziert und im Kontext des Objektes instance aufgerufen, sodass im Ergebnis der Wert der Eigenschaft property von instance zurückgegeben wird. Die Methode method wird also von Constructor.prototype an instance vererbt.

Nun haben wir in den Beispielen oben darüber hinaus zweimal die Methode hasOwnProperty auf dem Instanzobjekt aufgerufen, bei der es sich ebenfalls nicht um eine eigene Methode dieses Objektes handelt, sondern um eine Methode, die eigentlich auf dem Objekt Object.prototype definiert ist.

console.log(Object.prototype.hasOwnProperty('hasOwnProperty')); // true

const proto = Object.getPrototypeOf(Constructor.prototype);

console.log(proto === Object.prototype); // true

Da Object.prototype jedoch der Prototyp von Constructor.prototype ist, und weil wie gesehen beim Zugriff auf eine Eigenschaft oder Methode auch die jeweiligen Prototypen durchsucht werden, wird die Methode hasOwnProperty ebenso wie die Methode method beim Aufruf auf instance über die Prototypenkette referenziert.

Der Zweck der Eigenschaft prototype von Funktionsobjekten ist also, allen von der Funktion erzeugten Objekten Methoden und Eigenschaften zur Verfügung zu stellen, ohne diese auf den Objekten selbst definieren zu müssen.

Darum spricht man hier auch von Differenzieller Vererbung, das heißt, ein Prototyp stellt die Funktionalität zur Verfügung, die bei allen von diesem Prototypen erbenden Objekten vorhanden sein soll, während auf den Instanzobjekten selbst nur die Unterschiede, also die Differenzen definiert werden müssen.

Eine etwas ausführlichere Erklärung zu dem Thema bietet der Artikel Vererbung im Wiki.

Es soll allerdings nicht verschwiegen werden, dass die Erzeugung von Objekten durch den Aufruf einer Funktion oder Klasse mittels new nicht die einzige Möglichkeit darstellt, prototypische Vererbung zu implementieren.

const proto = {
  method ( ) {
    return this.property;
  }
};

const instance = Object.create(proto);

instance.property = 'value';

console.log(instance.method( )); // value

Zu diesem Zweck kann nämlich auch die Methode Object.create verwendet werden, welche ein neues planes Objekt erzeugt und als dessen Prototyp das Objekt installiert, welches der Methode als erstes Argument übergeben wurde.

Was nun die Methode call angeht, ist zu erwähnen, dass diese auf Function.prototype definiert ist, und da alle Funktionen von Function.prototype erben, kann sie entsprechend auch auf allen Funktionen aufgerufen werden.

Die Methode call ermöglicht es, eine Funktion in einem bestimmten Kontext aufzurufen. Das heißt, die Kontextvariable this der auf diese Weise aufgerufenen Funktion wird mit dem Wert initialisiert, welcher der Methode call als erstes Argument übergeben wurde.

function getThis (parameter) {
  console.log(parameter);
  return this;
}

const result = getThis.call('value', 'argument'); // argument

console.log(result); // value

Mit den weiteren an die Methode call übergebenen Argumenten wird dann die Funktion aufgerufen, auf der die Methode ausgeführt wird, weshalb der Parameter in dem Beispiel oben mit dem String 'argument' initialisiert wird.

Soll eine Funktion also explizit in einem bestimmten Kontext aufgerufen werden kann dies mit call bewerkstelligt werden. Oder mit der Methode Function.prototype.apply, welche die Argumente für den Aufruf in Form eines Arrays entgegennimmt.

Gruß,

Orlok