Benutzer:Rolf b/Operator new

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der Operator new dient dazu, neue Objekte zu erzeugen und sie mit Hilfe einer Funktion zu initialisieren. Die dafür verwendete Funktion wird Konstruktor genannt, das so entstandene Objekt bezeichnet man als Instanz des Konstruktors.

Einführende Erklärungen zum Konstruktor-Konzept finden Sie im OOP-Tutorial. Der Schwerpunkt dieses Referenzartikels ist der new-Operator.

Syntax

new Constructor([arguments])


An Stelle von Constructor setzen Sie den Namen der gewünschten Konstruktorfunktion ein. Viele dieser Funktionen benötigen Parameter, die zur Initialisierung des Objekts verwendet werden. Wenn Sie für den Aufruf der Konstruktorfunktion keine Argumente übergeben möchten, können Sie die runden Klammern auch weglassen.

Das Ergebnis des Operators ist ein neues Objekt, das durch die verwendete Konstruktorfunktion initialisiert wurde.

Beschreibung

Der new-Operator ist ein Mechanismus von JavaScript, um neue Objekte zu erstellen, mit einem Prototypen zu versehen und sie zu initialisieren. Er ist zusammen mit der class-Syntax für Klassendeklarationen verwendbar, aber nicht darauf angewiesen.

Der new-Operator ist nicht die einzige Möglichkeit, neue Objekte zu erstellen. Alternativ können Sie auch die Object.create-Methode oder ein Objektliteral benutzen.

Der JavaScript-Operator new erwartet eine Funktion und eine optionale Liste von Argumenten. Zum Beispiel: new Person('Mustermann', 'Erika'). Bei Um diesen Ausdruck auszuwerten, geschieht folgendes:

  1. Erstellen eines neuen, leeren Objekts O
  2. Den Wert von Person.prototype als Prototyp von O festlegen. Dabei handelt es sich um eine interne Eigenschaft aller Objekte, die laut ECMAScript-Spezifikation nicht direkt zugänglich ist. Wichtig: der Wert in Person.prototype ist nicht identisch mit dem Prototypen des Funktionsobjekts. Dazu gleich mehr.
  3. Die Konstruktorfunktion Person mit O als this-Objekt und den Argumenten ('Mustermann', 'Erika') aufrufen.
  4. O ist das Ergebnis der new-Operation.

Mit anderen Worten: JavaScript benutzt die Konstruktorfunktion, um den Prototypen für das neue Objekt zu liefern und ruft sie dann auf, um es zu initialisieren.

Beachten Sie: Dies ist das wichtigste, was new tut. Aber längst nicht alles!

Sie können das so erstellte Objekt nun verwenden. Die folgenden Abschnitte möchten Sie auf Komplikationen hinweisen, die durch falsche Programmierung entstehen können und die Sie vermeiden sollten. Dabei lernen Sie auch noch ein paar Aktivitäten kennen, die new durchführt.

Konstruktorfunktionen, Prototypen und ihre Beziehungen

Eine Möglichkeit, die Konstruktorfunktion eines Objekts herauszufinden, ist die constructor Eigenschaft dieses Objekts. Darin findet sind im Normalfall die Funktion, die für den new-Operator verwendet wurde.

constructor ist aber keine Eigenschaft des neuen Objekts, sondern seines Prototypen und wird vererbt. Das folgende Bild soll die Zusammenhänge zwischen Konstruktorfunktion, Prototypen und erzeugten Objekten darstellen.

Function-prototype-relation.svg

Test stellt eine Konstruktorfunktion dar. Funktionen sind Objekte, deshalb besitzt Test einen eigenen Prototypen, nämlich Function.prototype. Diese Prototypverbindung, die in der Grafik mit prototype bezeichnet ist, ist nicht zu verwechseln mit dem Objekt, das in der Eigenschaft prototype des Funktionsobjekts zu finden ist. Über die prototype-Verbindung erbt die Funktion Methoden wie call oder bind. Natürlich ist Function.prototype selbst ein Objekt und besitzt darum auch selbst einen Prototypen, den, der in Object.prototype zu finden ist.

Der Wert der Eigenschaft Test.prototype dient als Prototypobjekt für Objekte, die mittels new Test() erstellt werden. Sie müssen dieses Eigenschaft nicht selbst erzeugen, das tut JavaScript beim Erstellen des Funktionsobjekts automatisch. Dazu gehört auch, darin eine Eigenschaft constructor zu erstellen, in der das Funktionsobjekt gespeichert ist. Konstruktorfunktion und Prototyp zeigen also gegenseitig aufeinander.

Wenn Sie mit new Test() ein neues Objekt erstellen, entsteht die im Bild gezeigte Prototypenkette auf der rechten Seite: vom neuen Objekt zu Test.prototype und weiter zu Object.prototype. Auf diese Weise ist in den so erstellen Objekten eine constructor Eigenschaft verfügbar, die auf die Konstruktorfunktion zurückverweist und eine Unterscheidung von Objektklassen ermöglicht.

Sie können Test.prototype eigene Eigenschaften oder Methoden hinzufügen. Diese werden ebenfalls an die erstellten Objekt vererbt.

Vorsicht: Eigene Prototypobjekte

Wenn man an Test.prototype mehrere Methoden zuweisen möchte, kann man versucht sein, dies mittels eines Objektliterals zu tun. Das kann man zwar machen, aber dadurch geht die vom System vorbelegte constructor-Eigenschaft verloren. Objekte, die als Objektliteral erzeugt wurden, erhalten automatisch Object.prototype als Prototyp und erben damit die constructor-Eigenschaft von dort. Die Konsequenz: Objekte, die Sie mit einem solchen Prototypen erzeugen, würden behaupten, ihr Konstruktor sei Object.

Um die Wechselbeziehung zwischen Konstruktor und Prototyp aufrecht zu erhalten, müssen Sie selbst dafür sorgen, dass das so erstellte Prototypobjekt eine Eigenschaft constructor aufweist, in der die Konstruktorfunktion eingetragen ist. Und damit ist alles gut.

Objektliteral als Prototyp
function Test() {
   this.wert = 42;
}
Test.prototype = {
   constructor: Test,
   demo(x) { return x * this.wert; }
}

Besonderheiten für die Konstruktorfunktion

Rückgabewert

Ein Punkt, der in der Todo-Liste des new Operators unterschlagen wurde, ist die Auswertung des Rückgabewertes der Konstruktorfunktion. Das normale Verhalten von Konstruktoren ist, dass sie nichts zurückgeben, also implizit mit return undefined; enden. Findet der new-Operator undefined oder einen der anderen primitiven Typen vor, ist sein Ergebnis ganz normal das erzeugte Objekt.

Die Konstruktorfunktion kann aber auch ein beliebiges Objekt zurückgeben. In diesem Fall verwendet new dieses Objekt als Ergebnis. Das erscheint unsinnig - warum mit new ein neues Objekt erzeugen, wenn man es dann nicht benutzt. Es gibt aber tatsächlich Situationen, wo ein solches Konstrukt erforderlich ist. Sie sollten dann aber darauf achten, dass das „gefälschte“ Objekt so gut gefälscht ist, dass das Prototyp-Objekt der Konstruktorfunktion einbezogen wird und die constructor-Eigenschaft korrekt auf die Konstruktorfunktion zeigt.

new.target - Aufruf des Konstruktors ohne new

Konstruktoren sind Funktionen. Es ist deshalb in JavaScript aus Syntaxsicht völlig legitim, sie auch ohne den new-Operator aufzurufen. Der gravierende Unterschied ist, dass ohne new kein neues Objekt erstellt wird und in this etwas anderes steht.

Wenn Ihr Code im strict mode läuft, sind die möglichen Schäden begrenzt, denn dann steht in this der Wert undefined. Wenn die Konstruktorfunktion zu this Eigenschaften hinzufügen möchte, bricht die Programmausführung mit einem TypeError ab. Anders ist es ohne "use strict". Jetzt steht in this das globale Objekt, und die neuen Eigenschaften werden dort hinzugefügt. Ein Fehler, der beim Test sicherlich irgendwann auffällt, weil sich die Objekte dieses Konstruktors gegenseitig ihre Eigenschaften überschreiben, aber ein vergessenes new kann schwer zu finden sein.

Andererseits gibt es auch Funktionen, die eigenständig und als Konstruktor verwendbar sind. Als Beispiel seien die Konstruktorfunktionen für die Objektwrapper der primitiven Typen genannt - Number, Boolean und String. Number("123") konvertiert die Zeichenkette "123" in den primitiven Number-Wert 123, während new Number("123") ein Number-Objekt mit dem Wert 123 erzeugt.

Die Unterscheidung zwischen "eigenständiger Aufruf" und "Aufruf als Konstruktor" war vor ECMAScript 2015 nur durch eine Analyse von this möglich und hing auch davon ab, ob der Code im strikten Modus lief oder nicht. Mit der 2015-Version wurde die Unterscheidung durch die Pseudoeigenschaft new.target systematisiert. Sie darf nur innerhalb von Funktionen verwendet werden, nicht auf globaler Ebene. Ihr Wert hängt davon ab, ob die Funktion, in der sie abgefragt wird, ohne oder mit new aufgerufen wurde. In einem normalen Aufruf enthält new.target den Wert undefined, bei einem Aufruf als Konstruktor dagegen das Funktionsobjekt der Funktion, in der sie verwendet wird. Wenn Sie Vererbung mit der class-Syntax programmieren, liefert new.target auch im mit super() aufgerufenen Konstruktor der Basisklasse die Konstruktorfunktion, die zusammen mit new verwendet wurde.

Verwendung von new.target
function Person(name) {
   if (!new.target)
      throw new TypeError("Person ist ein Konstruktor!");
   this.name = name;
}
let oderWas = Person("Müller");      // TypeError
Hinweis:
Wenn Sie new.target innerhalb einer Pfeilfunktion verwenden, verwendet die Pseudoeigenschaft den Kontext der Funktion, in der die Pfeilfunktion notiert wurde, weil Pfeilfunktionen keinen eigenen Kontext erzeugen.

Superklassen

Die Implementierung von Klassenhierarchien ist mit den klassischen Konstruktorfunktionen von JavaScript möglich, aber nicht einfach. Das class-Schlüsselwort von EcmaScript 2015 hat hier vieles vereinfacht, und das Schlüsselwort super ins Spiel gebracht. Dieses Schlüsselwort können Sie nur innerhalb der class-Syntax verwenden. Wenn Sie das tun, ist dieses Kapitel für Sie nicht relevant.

Schauen wir uns aber an, wie man Klassenhierarchien ohne class erzeugt:

Superklassen vor ECMAScript 2015
function Person(familienname, vorname) {
   this.familienname = familienname;
   this.vorname = vorname;
}
Person.prototype.vollerName = function() { return this.vorname + " " + this.familienname; };

function Angestellter(familienname, vorname, personalnr) {
   Person.call(this, familienname, vorname);
   this.personalNummer = personalnr;
}
Angestellter.protoype = Object.create(Person.prototype);
Angestellter.prototype.constructor = Angestellter;
Angestellter.prototype.bucheBeginn(zeit) = function(z) { ... };

Damit ein Angestellter oder eine Angestellte die vollerName-Methode von Person.prototype erben kann, bekommt Angestellter als Prototyp für neue Objekte ein leeres Objekt, das Person.prototype als Prototyp hat. Wie vorhin erläutert, führt das zu einem Prototypen mit falscher constructor-Eigenschaft, die deswegen im nächsten Schritt repariert wird. Diesem neuen Angestellten-Prototypen kann man nun noch weitere Methoden geben, die nur für Angestellte relevant sind.

Allerdings wird es nun im Angestellter-Konstruktor mühsam. Denn JavaScript weiß nicht wirklich etwas von der Objekthierarchie und ruft deswegen nur die Konstruktorfunktion für Angestellter auf. Wenn eine Basisklasse mit Konstruktor vorliegt, muss dieser aber ebenfalls gerufen werden, um den Basisklassen-Anteil des neuen Objekts zu initialisieren. Deswegen finden Sie mit Person.call(this, ...) einen expliziten Aufruf des Basisklassen-Konstruktors, für den this auf das neue Objekt gesetzt wird.

Im Artikel zum in JavaScript eingebauten Objekt Object finden Sie eine Beschreibung, wie Sie einfache Objekte erzeugen können. Solche Objekte verwenden den Basisprototyp in Object.prototype und müssen im Übrigen an der Stelle der Erzeugung Eigenschaft für Eigenschaft initialisiert werden. Wenn Sie Objekte erzeugen wollen, die einem bestimmten Schema gehorchen, können Sie das entweder durch eine Hilfsfunktion oder Programmierdisziplin erreichen, oder Sie nutzen den Komfort, den Ihnen der new-Operator und eine Konstruktorfunktion bieten.

An Stelle einer Konstruktorfunktion können Sie auch die neuere class-Syntax von ECMAScript 2015 (ES6) verwenden und in einer class eine constructor-Methode erstellen. Sie erhalten das gleiche Ergebnis.

Etliche Konstruktorfunktionen sind Teil des Sprachumfangs von JavaScript, wie zum Beispiel Object oder Array. Andere werden vom Browser bereitgestellt, beispielsweise CustomEvent oder URL. Natürlich können Sie auch eigene Funktionen als Konstruktorfunktionen verwenden.



Wenn Sie in JavaScript eine Funktion erstellen, sind grundsätzlich zwei Möglichkeiten vorgesehen, diese Funktion zu nutzen. Die eine Möglichkeit ist der gewöhnliche Aufruf, entweder eigenständig, oder als Methode eines Objekts. Dabei entsteht kein neues Objekt. Die Funktion erhält einfach die übergebenen Argumente als Parameter, kann sie verabeiten und einen Wert zurückgeben. Wenn sie als Methode genutzt wird, steht in der Systemvariablen this auch das Objekt zur Verfügung, für das sie als Methode dient.

Diese Art des Funktionsaufrufs wird im Artikel über Funktionen in JavaScript ausführlich beschrieben und hier als bekannt vorausgesetzt. Zu erwähnen wäre lediglich, dass der Wert der Kontextvariablen this je nach Art des Aufrufs und abhängig davon, ob der Code im strict mode läuft oder nicht, den Wert null, das globale Objekt oder ein zuvor existierendes Objekt ist. Sie können aus einen solchen Funktionsaufruf mit der return Anweisung beenden und dieser Anweisung auch einen Wert übergeben, der dann zum Ergebnis des Funktionsaufrufs wird.

Anders ist es, wenn eine Funktion zusammen mit dem new-Operator verwendet wird. JavaScript erzeugt nun zunächst einmal ein vollständig neues Objekt. In diesem Objekt werden zwei Informationen gespeichert:

constructor
In dieser Eigenschaft wird eine Referenz des Funktionsobjekts hinterlegt, das mit new verwendet wurde
[[Prototype]]
Dies ist eine interne Eigenschaft, auf die Sie nicht direkt zugreifen können (Sie können sie mit der Methode Object.getPrototypeOf auslesen). Es handelt sich um den Prototypen, der für das neue Objekt verwendet wird, und er wird der Eigenschaft prototype entnommen, die Sie an der Konstruktorfunktion hinterlegen.

Die Eigenschaften und Methoden des Prototypen werden auf das neue Objekt vererbt. Sie können die Eigenschaften auslesen und die Methoden aufrufen, als wären sie in dem neuen Objekt gespeichert. Wenn Sie Eigenschaften Werte zuweisen, werden sie aber direkt im Objekt gespeichert und nicht im Prototypen.

Nun wird die Konstruktorfunktion aufgerufen, und bekommt in this das neue Objekt bereitgestellt. Sie kann nun noch weitere Eigenschaften hinzufügen und das neue Objekt so initialisieren. Nachdem die Konstruktorfunktion endet, ist das so erzeugte und initialisierte Objekt das Ergebnis der new-Operation.

Beispiel
function TestObjekt() {
  this.wert = 123;
}

TestObjekt.prototype = {
   einsMehr: function() {
      return this.wert + 1;
   }
};

const instanz = new TestObjekt;

console.log(typeof instanz);           // Ausgabe: object
console.log(instanz.constructor.name); // Ausgabe: TestObject
console.log(instanz.wert);             // Ausgabe: 123
console.log(instanz.einsMehr());       // Ausgabe: 124

Wenn Sie sich fragen, was das für ein Ding ist, das an TestObjekt.prototype zugewiesen wird, lesen Sie bitte unter Objektliterale nach.

Die Konstruktorfunktion erwartet keine Parameter. Wie schon eingangs erwähnt, kann dann man die runden Klammern, in denen Argumente notiert würden, ganz weglassen.

Das neue Objekt, das durch new erzeugt wurde, wird der Konstruktorfunktion in this bereitgestellt und bekommt dort eine Eigenschaft wert, in der die Zahl 123 gespeichert wird. Nach Rückkehr aus der Konstruktorfunktion wird das neue Objekt in der Variablen instanz gespeichert.

Für JavaScript ist dies nun erst einmal ein Objekt wie alle anderen. Der typeof-Operator gibt einfach 'object' zurück. Wenn Sie wissen möchten, welcher Konstruktor dieses Objekt erzeugt hat, können Sie die constructor-Eigenschaft nutzen, die vom new Operator angelegt wurde. Sie erhalten so das Funktionsobjekt der Konstruktorfunktion und können es entweder direkt mit Ihrem Funktionsobjekt vergleichen, oder die name-Eigenschaft verwenden, die alle Funktionen besitzen.

Die dritte Konsolenausgabe zeigt Ihnen, dass sich die wert-Eigenschaft, die vom Konstruktor an this gespeichert wurde, in dem Objekt wiederfindet, das in instanz abgelegt wurde, und in der vierten Ausgabe wird die Methode einsMehr aufgerufen, die als Methode auf dem Prototyp-Objekt bereitgestellt wurde, das zuvor in der prototype-Eigenschaft der Konstruktorfunktion hinterlegt wurde. Die wichtige Erkenntnis ist hier, dass diese Methode unter this das Instanzobjekt vorfindet, und nicht das Prototypobjekt. Deswegen kann die Methode die Eigenschaft wert nutzen. Schauen wir uns das mit Hilfe der Methode HasOwnProperty einmal genauer an. Nehmen Sie bitte an, dass das letzte Beispiel mit diesen Zeilen fortgesetzt wird:

Beispiel
console.log(instanz.hasOwnProperty("wert"));   // true
console.log(instanz.hasOwnProperty("einsMehr"));   // false
console.log(Object.getPrototypeOf(instanz).hasOwnProperty("einsMehr"));   // true - Aha!

hasOwnProperty verrät uns, dass wert eine Eigenschaft von instanz und einsMehr eine Eigenschaft des Prototypen von instanz ist.

Haben Sie jetzt Fragezeichen vor Augen? Wenn nicht - hier ist das erste: was ist das für ein Gewimmel an Klammern, das an TestObject.prototype zugewiesen wird? Es handelt sich um ein Objektliteral, mit dem das Prototypobjekt erzeugt wird. Dieses Prototypobjekt enthält die Methode einsMehr, die im Beispiel an die Objekte vererbt werden soll, die vom TestObjekt-Konstruktor


Wieso finde ich mit hasOwnProperty eine Methode? Na gut, das ist einfach: Methoden sind nichts weiter als Objekteigenschaften, die Funktionen enthalten. 

Aber schwieriger ist dieses: Wo kommt die Methode hasOwnProperty her, bitte schön? Die wurde doch weder vom Konstruktor noch vom Prototypen bereitgestellt. Wenn Sie dem Link auf dem Methodennamen gefolgt sind, dann sehen Sie dort, dass diese Methode von Object.prototype vererbt wird. Aber wo haben wir angegeben, dass wir das Prototyp-Objekt verwenden wollen, das in Object.prototype zu finden ist?

Die Antwort heißt: Nirgends. Und das ist auch überflüssig, denn jedes Objekt, das Sie als Objektliteral anlegen, bekommt automatisch den Prototypen aus Object.prototype zugewiesen. Und das bedeutet: Ihr Prototypobjekt, das die einsMehr-Methode bereitstellt, hat selbst einen Prototypen. Das instanz-Objekt erbt von einer ganzen Ahnenreihe an Prototypen! Wenn Sie das vermeiden wollen, müssen Sie mit Hilfe von Object.create ausdrücklich null als Prototyp spezifizieren, um ein prototypfreies Objekt zu erhalten. Object.prototype ist so eines.

Unter dem Wiki-Stichwort Prototyp finden Sie weitere Erklärungen zum Thema Prototypen.

Das nächste Beispiel zeigt, daass Sie einer Konstruktorfunktion auch Argumente übergeben und in der Funktion nach Belieben verwenden können. Dafür notieren Sie dann wieder die runden Klammern, wie bei einem regulären Funktionsaufruf. Aber Vorsicht vor dummen Fehlern - ein Konstruktor ist keine normale Funktion!

Beispiel
function Punkt(x, y) {
   this.x = x;
   this.y = y;
}
function Dumm(z) {
   this.z = z;
   return z;
}

const punkt = new Punkt(3,4);
const dumm = new Dumm(5);
console.log(punkt.x, punkt.y); // 3 4
console.log(dumm.z);           // undefined - ups!
console.log(dumm);             // 5 - return z; ist schuld!

Was Sie nicht tun sollten, ist das Zurückgeben eines Wertes aus der Konstruktorfunktion. Denn dieser Wert ersetzt das neue Objekt, das von new angelegt wurde. Es kann Sonderfälle geben, wo dieses Verhalten erwünscht ist. Es ist aber nicht der Normalfall, und es widerspricht auch dem Konzept des new-Operators.

Hinweis:
Es ist in JavaScript üblich, Funktionsnamen mit einem Kleinbuchstaben beginnen zu lassen. Konstruktorfunktionen weichen von dieser Konvention bewusst ab, um sie von normalen Funktionen klar abzugrenzen.

Fallstricke

Einen davon haben Sie schon kennengelernt: Die Rückgabe eines Wertes aus einer Konstruktorfunktion. JavaScript verwendet diesen Rückgabewert dann als das Ergebnis des new-Operators.

Ein weiterer ist der irrtümliche Aufruf einer Konstruktorfunktion als normale Funktion.

Werden beim Aufruf einer Funktion als Konstruktor, so wie im letzten Beispiel, keine Argumente übergeben, dann können die runden Klammern nach dem Bezeichner der Funktion weggelassen werden, da der Aufruf selbst bereits Teil der Semantik des Operators new ist. Sollen hingegen Argumente an den Konstruktor übergeben werden, dann sind auch hier runde Klammern zu notieren, welche die Liste der Argumente umschließen, wie bei einem normalen Funktionsaufruf auch. Übergebene Werte können wie in dem Beispiel oben dazu verwendet werden, um Eigenschaften auf dem erzeugten Objekt zu definieren, welches über die Kontextvariable this referenziert wird.

Beispiel
const result = instance.hasOwnProperty('name');

console.log(result); // true


Hierbei ist zu beachten, dass Eigenschaften oder Methoden, die innerhalb des Konstruktors durch Zuweisung oder ausdrückliche Definition auf dem Objekt angelegt werden, welches über die Variable this referenziert wird, zu eigenen Eigenschaften oder Methoden dieses Objektes werden, weshalb die Überprüfung mit hasOwnProperty für die im letzten Beispiel direkt auf dem Objekt angelegte Eigenschaft name den booleschen Wert true zurückgibt.


Beispiel
function createObject (value) {
  return {
    name : value
  };
}

const object = createObject('object');

console.log(object.hasOwnProperty('name')); // true


Das letzte Codebeispiel demonstriert eine Objekt-Konstruktion mit einer einfachen Funktion – ganz ohne new und Konstruktorausruf.

Ein Gedankenspiel:
Die im Konstruktor (egal, ob mit new oder als "einfaches" return-Objekt) deklarierte Methoden werden immer mit dem gleichen Quelltext in jedes erzeugte Objekt geschrieben, obwohl mit this nur auf das eigene Objekt zugegriffen wird? Zehn neue Objekte = zehn Kopien des gleichen Codes?! Wäre es nicht eleganter und effizienter, die Methode nur einmal im Speicher abzulegen?

Genau das ist das Besondere an der new-Methode: Sie kann Methoden der Prototypen-Eigenschaft dem neuen Objekt vererben.


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

Constructor.prototype.showString = function () { //Prototyp-Methode
  console.log(this.key);
}

var newObject = new Constructor("Hello!");
newObject.showString(); //Hello!


Die Funktion Constructor ist noch normal. showString() dagegen wird nicht direkt an Constructor gebunden, sondern an die Eigenschaft prototype. Das anschließende new instantiiert dann im ersten Schritt Constructor.prototype und führt im zweiten Schritt die Funktion Constructor() in diesem neuen Kontext aus. newObject.showString() führt dann eigentlich die Funktion Constructor.prototype.showString() ausgeführt, dabei zeigt this auf newObject und showString() gibt Eigenschaft newObject.key aus.

Werfen wir einen Blick auf die beteiligten:

  1. console.log(Constructor.showString);
    
    = undefined
    es gibt also keine Funktion Constructor.showString
  2. console.log(newObject.showString);
    
    = [Function]
    hinter newObject.showString verbirgt sich eigentlich die Funktion Constructor.prototype.showString(), nur ausgeführt im Kontext von newObject. this verweist newObject
    (Firefox 48 ist gesprächiger: function Constructor.prototype.showString())
  3. console.log(Constructor.prototype);
    
    = Constructor { showString: [Function] }
    zeigt den Objekt-Charakter der Prototypen-Eigenschaft.


Der Vorteil der Kombination von new und Prototypen liegt bei der zentralen Definition von Methoden und Eigenschaften im Prototypen und ihrer Vererbung über die Prototypenkette beim Konstruktoraufruf an die neue Instanz. Die Methoden und Eigenschaften sind an den Prototyp delegiert, müssen also nicht mehr auf jeder Instanz definiert werden.


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

const prototype = Object.getPrototypeOf(new Constructor);

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


Die Methode getPrototypeOf gibt den Prototyp eines Objektes aus, der in diesem Beispiel der Konstante prototype zugewiesen wird. Der anschließende Vergleich beweist, dass prototype eine neue Instanz von Constructor.prototype ist.


Beispiel
function Constructor ( ) {
  return {
    key : 'value',
  }
}

Constructor.prototype.methode = function (){
  return "Prototypen-Methode";
}

const instance = new Constructor;
const prototype = Object.getPrototypeOf(instance);

console.log(Constructor.prototype === prototype); // false


Rückgabewerte via return in der Konstruktorfunktion sind problematisch. Sind es Skalare (Boolean, String oder Number), verwendet new weiterhin den Prototypen und die neue Instanz zurück. Gibt der Konstruktor dagegen ein eigenes Objekt zurück, verwendet new dieses Rückgabeobjekt statt des eigentlich beabsichtigten Prototypen! Im Beispiel sind eine Konstruktorfunktion Constructor() mit eigenem Rückgabeobjekt und eine Prototypenmethode methode() definiert. new gibt dennoch das Objekt mit dem Wertepaar key: "value" zurück. Der Vergleich auf den Prototypen zeigt den Unterschied (=false).

Verschiedene Arten von Funktionen

Wie im letzten Abschnitt gesehen, besitzen alle gewöhnlichen Funktionen eine interne Methode [[Construct]], unabhängig davon, ob sie deklariert oder als Ausdruck notiert werden. Dem zur Folge können alle diese Funktionen mittels new als Konstruktor aufgerufen werden. Jedoch ist es so, dass nicht alle Funktionsobjekte über eine solche interne Methode verfügen.


Beispiel
const symbol = new Symbol('description'); // Type Error

console.log(symbol);


Zum Beispiel besitzen einige der eingebauten Standardobjekte, wie etwa das Funktionsobjekt Symbol, keine solche Methode für den Konstruktorenaufruf, weshalb der Versuch eines Aufrufs mittels new hier einen Typfehler produziert. Solche Funktionen können also nur über die interne Methode [[Call]] aufgerufen werden, die grundsätzlich jedes Funktionsobjekt besitzt.


Beispiel
const Constructor = value => {
  'use strict';
  this.key = value;
};

const instance = new Constructor('value'); // Type Error


Die in der sechsten Edition des Standards eingeführten Pfeilfunktionen können ebenfalls nicht als Konstruktor aufgerufen werden. Ein solcher Aufruf führt entsprechend auch hier zu einen Fehler. Bei diesen Funktionen ist darüber hinaus zu beachten, dass sie über keine eigene Bindung für die Kontextvariable this verfügen, das heißt, hier wird grundsätzlich der Wert von this der umgebenden lexikalischen Umgebung referenziert. Dem zur Folge hat die Anweisung, die Funktion im strict mode auszuführen, hier keinen Einfluss auf den Wert von this innerhalb der Funktion. Würde diese also über [[Call]] aufgerufen, dann würde hier eine globale Variable produziert, da der Ausführungskontext in dem die Funktion definiert wurde im normalen Modus ausgeführt wird und this hier entsprechend auf das globale Objekt verweist.


Beispiel
function * Generator ( ) {
  console.log(this);
  yield 'value';
}

const instance = new Generator; // Type Error


Auch Generatorfunktionen verfügen über keine interne Methode [[Construct]] und können dem zur Folge nicht als Konstruktor aufgerufen werden. Dies wäre hier allerdings auch nicht besonders sinnvoll, da diese Funktionen bereits bei einem normalen Funktionsaufruf über [[Call]] ein Objekt erzeugen und zurückgeben.


Beispiel
class CustomType {
  constructor ( ) {
    this.type = this.constructor.name;
  }
  logType ( ) {
    console.log(this.type);
  }
}

const instance = new CustomType;

instance.logType( ); // CustomType


Jedenfalls ist es nicht nur möglich, sondern auch zwingend erforderlich, eine Klasse als Konstruktor aufzurufen. Diese in der sechsten Edition des Standards eingeführten Funktionsobjekte besitzen also eine interne Methode [[Construct]]. Tatsächlich können Klassen auch nur über diese Methode aufgerufen werden, da hier anders als in den vorangegangenen Beispielen ein Aufruf über [[Call]] einen Fehler produziert.

Unbeabsichtigte Erzeugung globaler Variablen

Wie eingangs bereits erwähnt, wird die Kontextvariable this einer gewöhnlichen Funktion bei einem normalen Funktionsaufruf über die interne Methode [[Call]] mit einer Referenz auf das globale Objekt initialisiert, also in Browsern das Objekt window, wenn die Funktion im normalen Modus ausgeführt wird und nicht im Strict Mode.


Beispiel
function Constructor ( ) {
  this.key = 'value';
}

const instance = Constructor( );

console.log(key); // value


Wird also wie in dem Beispiel oben eine Funktion definiert die als Konstruktor dienen soll, beim Aufruf jedoch der Operator new vergessen, dann weist die Kontextvariable this im normalen Ausführungsmodus auf das globale Objekt window, sodass durch die innerhalb der Funktion vorgenommen Zuweisungen an this keine Eigenschaften auf der Instanz definiert werden, sondern Eigenschaften des Objektes window, die damit automatisch zu globalen Variablen werden. Es wird durch das Weglassen des Operators new also kein Fehler erzeugt, obwohl dies hier natürlich wünschenswert wäre.


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

const instance = Constructor( ); // Type Error


Wird die Funktion hingegen im Strict Mode ausgeführt, dann wird this bei einem normalen Funktionsaufruf nicht mit einer Referenz auf das globale Objekt initialisiert und der Wert bleibt entsprechend undefined. Da es sich bei dem Wert undefined jedoch um einen primitiven Wert handelt und nicht um ein Objekt, wird bei dem Versuch der Zuweisung innerhalb der aufgerufenen Funktion nun eine Ausnahme geworfen, der Fehler, den Operator new vergessen zu haben, wird also sofort offenbar und kann behoben werden. Es ist also grundsätzlich zu empfehlen, Funktionen die als Konstruktor dienen sollen immer im Strict Mode auszuführen.


Beispiel
function Constructor (value) {
  if (! new.target) {
    throw new TypeError( );
  }
  this.key = value;
}

const instance = Constructor('value'); // Type Error


Seit der sechsten Edition des Standards gibt es allerdings eine Alternative zur Ausführung im Strict Mode, nämlich den Operator new.target. Dieser wird bei einem Aufruf einer Funktion als Konstruktor mit einer Referenz auf ebendiese Funktion initialisiert, also auf das Ziel des Aufrufs mittels new, und bei einem normalen Funktionsaufruf ist der Wert dieses Operators entsprechend undefined. Es kann also auch wie in dem Beispiel oben geprüft werden, mit welchem Wert der Operator new.target initialisiert wurde und in dem Fall, dass dessen Wert undefined ist, weil der Operator new vergessen wurde, kann selbst ein Fehler geworfen werden.

Alternativen zum Operator new

Statt den Operator new für den Konstruktorenaufruf zu verwenden, kann zu diesem Zweck auch die Methode construct des Standardobjektes Reflect aufgerufen werden, welche als erstes Argument das entsprechende Funktionsobjekt erwartet und als optionales zweites Argument ein Array-ähnliches Objekt mit den Argumenten, die beim Aufruf übergeben werden sollen.


Beispiel
class CustomType {
  logTrue ( ) {
    console.log(true);
  }
}

const instance = Reflect.construct(CustomType);

instance.logTrue( ); // true


Auch hier wird also die jeweilige Funktion über die interne Methode [[Construct]] aufgerufen, unabhängig davon ob es sich um eine gewöhnliche Funktion handelt, oder wie hier in dem Beispiel um eine Klasse. Es ist allerdings auch hier zu beachten, dass sowohl die Klassensyntax als auch das eingebaute Objekt Reflect mit seinen Methoden erst seit der sechsten Edition des Standards ein Teil der Sprache ist. Vor der Verwendung dieser Features sollte also zunächst die Kompatibilität geprüft werden.


Beispiel
const prototype = {
  logTrue ( ) {
    console.log(true);
  }
};

const object = Object.create(prototype);

object.logTrue( ); // true


Schließlich kann zur Erzeugung eines Objektes mit einem bestimmten Prototypen auch die Methode create des Konstruktors Object verwendet werden, das heißt, die Vererbung von Eigenschaften und Methoden kann auch gänzlich ohne einen Konstruktorenaufruf implementiert werden.

Weblinks