JavaScript/Operatoren/new

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der Operator new ruft eine Funktion als Konstruktor auf. Dabei wird ein neues Objekt erzeugt, welches man als Instanz des Konstruktors bezeichnet. Unter Verwendung des Operators new können sowohl Instanzen eingebauter Konstruktoren wie beispielsweise Object, als auch Instanzen selbstdefinierter Konstruktoren erzeugt werden. Der Aufruf einer Funktion als Konstruktor stellt eine Möglichkeit dar, die Vererbung von Eigenschaften und Methoden zu implementieren, da der Prototyp eines auf diese Weise erzeugten Objektes standardmäßig das Objekt ist, welches in der Eigenschaft prototype des Konstruktors hinterlegt ist.


Syntax

new Constructor([arguments])


Beschreibung[Bearbeiten]

Wird eine gewöhnliche Funktion definiert, entweder durch Deklaration oder über einen Ausdruck, dann wird hierdurch ein Funktionsobjekt erzeugt, welches über zwei interne Methoden für den Aufruf verfügt, die Methode [[Call]] für den normalen Funktionsaufruf und die Methode [[Construct]] für den Aufruf als Konstruktor.


Beispiel
'use strict';

function normal ( ) {
  console.log(typeof this);
}

const value = normal( ); // undefined

console.log(typeof value); // undefined


Im Beispiel oben wird zunächst eine gewöhnliche Funktion deklariert. Diese wird danach über ihren Bezeichner referenziert und durch die dahinter notierten runden Klammern, den sogenannten Call Operator, aufgerufen. Hierbei handelt es sich um einen normalen Funktionsaufruf über die interne Methode [[Call]]. Je nach Ausführungsmodus verweist die Kontextvariable this der Funktion bei dieser Form des Aufrufs auf das globale Objekt, oder sie besitzt den primitiven Wert undefined. Der Rückgabewert der Funktion ist bei einem normalen Funktionsaufruf, sofern nichts anderes bestimmt wurde, ebenfalls undefined.


Beispiel
'use strict';

function Constructor ( ) {
  console.log(typeof this);
}

const instance = new Constructor; // object

console.log(typeof instance); // object


In diesem Beispiel wird ebenfalls eine Funktion deklariert. Diese wird im Gegensatz zum letzten Beispiel jedoch unter Verwendung des Operators new als Konstruktor aufgerufen. Das heißt, der Aufruf erfolgt hier nicht über die interne Methode [[Call]], sondern über die Methode [[Construct]]. Infolgedessen wird ein neues Objekt erzeugt und dieses wird an die Kontextvariable this gebunden, weshalb als Ergebnis der Prüfung mit dem Operator typeof der String object ausgegeben wird. Dieses Objekt, die Instanz, ist darüber hinaus auch der Rückgabewert der Funktion.


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

const instance = new Constructor('instance');

console.log(instance.name); // instance


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 Prototype 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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]