Understanding JavaScript Constructors

Avatar of Faraz Kelhini
Faraz Kelhini am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Der folgende Beitrag ist ein Gastbeitrag von Faraz Kelhini. Einige dieser Dinge liegen außerhalb meiner Komfortzone, also bat ich Kyle Simpson, ihn für mich technisch zu prüfen. Kyles Antwort (die wir während einer Office Hours-Sitzung gaben) war sehr interessant. Sie lautete: 1) Dieser Artikel ist technisch einwandfrei. JavaScript hat keine Klassen im herkömmlichen Sinne und dies ist der Weg, wie die meisten Leute sie hineinpressen. 2) Wir sollten aufhören, sie hineinzupressen. JavaScript hat Objekte und wir können sie so verwenden, wie sie gedacht sind, um die gleichen Dinge zu tun. Kyle nennt es OLOO (Objects Linked to Other Objects). Hier ist eine Einführung. Ich denke, es gibt Wert darin, beides zu lernen.

Ein gutes Verständnis von Konstruktoren ist entscheidend, um die JavaScript-Sprache wirklich zu verstehen. Technisch gesehen hat JavaScript keine Klassen, aber es hat Konstruktoren und Prototypen, um ähnliche Funktionalitäten in JavaScript zu bringen. Tatsächlich fungiert die Klassen Deklaration, die in ES2015 eingeführt wurde, einfach als syntaktischer Zucker über der bestehenden Prototypen-basierten Vererbung und fügt der Sprache keine zusätzliche Funktionalität hinzu.

In diesem Tutorial werden wir Konstruktoren im Detail untersuchen und sehen, wie JavaScript sie zur Erstellung von Objekten nutzt.

Konstruktoren erstellen und verwenden

Konstruktoren sind wie normale Funktionen, aber wir verwenden sie mit dem Schlüsselwort new. Es gibt zwei Arten von Konstruktoren: eingebaute Konstruktoren wie Array und Object, die zur Laufzeit automatisch in der Ausführungsumgebung verfügbar sind; und benutzerdefinierte Konstruktoren, die Eigenschaften und Methoden für Ihren eigenen Objekttyp definieren.

Ein Konstruktor ist nützlich, wenn Sie mehrere ähnliche Objekte mit denselben Eigenschaften und Methoden erstellen möchten. Es ist üblich, den Namen von Konstruktoren großzuschreiben, um sie von normalen Funktionen zu unterscheiden. Betrachten Sie den folgenden Code

function Book() { 
  // unfinished code
} 

var myBook = new Book();

Die letzte Zeile des Codes erstellt eine Instanz von Book und weist sie einer Variablen zu. Obwohl der Book-Konstruktor nichts tut, ist myBook trotzdem eine Instanz davon. Wie Sie sehen können, gibt es keinen Unterschied zwischen dieser Funktion und normalen Funktionen, außer dass sie mit dem Schlüsselwort new aufgerufen wird und der Funktionsname großgeschrieben ist.

Bestimmen des Typs einer Instanz

Um herauszufinden, ob ein Objekt eine Instanz eines anderen ist, verwenden wir den Operator instanceof

myBook instanceof Book    // true
myBook instanceof String  // false

Beachten Sie, dass dies einen Fehler auslöst, wenn die rechte Seite des instanceof-Operators keine Funktion ist

myBook instanceof {};
// TypeError: invalid 'instanceof' operand ({})

Eine weitere Möglichkeit, den Typ einer Instanz zu ermitteln, ist die Verwendung der Eigenschaft constructor. Betrachten Sie das folgende Codefragment

myBook.constructor === Book;   // true

Die constructor-Eigenschaft von myBook verweist auf Book, daher gibt der strikte Gleichheitsoperator true zurück. Jedes Objekt in JavaScript erbt eine constructor-Eigenschaft von seinem Prototypen, die auf die Konstruktorfunktion verweist, die das Objekt erstellt hat

var s = new String("text");
s.constructor === String;      // true

"text".constructor === String; // true

var o = new Object();
o.constructor === Object;      // true

var o = {};
o.constructor === Object;      // true

var a = new Array();
a.constructor === Array;       // true

[].constructor === Array;      // true

Beachten Sie jedoch, dass die Verwendung der constructor-Eigenschaft zur Überprüfung des Typs einer Instanz im Allgemeinen als schlechte Praxis angesehen wird, da sie überschrieben werden kann.

Benutzerdefinierte Konstruktorfunktionen

Ein Konstruktor ist wie ein Ausstecher zum Erstellen mehrerer Objekte mit denselben Eigenschaften und Methoden. Betrachten Sie das folgende Beispiel

function Book(name, year) {
  this.name = name;
  this.year = '(' + year + ')';
}

Der Book-Konstruktor erwartet zwei Parameter: name und year. Wenn der Konstruktor mit dem Schlüsselwort new aufgerufen wird, weist er die empfangenen Parameter der name- und year-Eigenschaft der aktuellen Instanz zu, wie unten gezeigt

var firstBook = new Book("Pro AngularJS", 2014);
var secondBook = new Book("Secrets Of The JavaScript Ninja", 2013); 
var thirdBook = new Book("JavaScript Patterns", 2010);
 
console.log(firstBook.name, firstBook.year);           
console.log(secondBook.name, secondBook.year);           
console.log(thirdBook.name, thirdBook.year);  

Dieser Code gibt Folgendes auf der Konsole aus

Wie Sie sehen, können wir schnell eine große Anzahl verschiedener Buchobjekte erstellen, indem wir den Book-Konstruktor mit verschiedenen Argumenten aufrufen. Dies ist genau das gleiche Muster, das JavaScript in seinen eingebauten Konstruktoren wie Array() und Date() verwendet.

Die Methode Object.defineProperty()

Die Methode Object.defineProperty() kann innerhalb eines Konstruktors verwendet werden, um alle notwendigen Eigenschafteneinrichtungen durchzuführen. Betrachten Sie den folgenden Konstruktor

function Book(name) { 
  Object.defineProperty(this, "name", { 
      get: function() { 
        return "Book: " + name;       
      },        
      set: function(newName) {            
        name = newName;        
      },               
      configurable: false     
   }); 
}

var myBook = new Book("Single Page Web Applications");
console.log(myBook.name);    // Book: Single Page Web Applications

// we cannot delete the name property because "configurable" is set to false
delete myBook.name;    
console.log(myBook.name);    // Book: Single Page Web Applications

// but we can change the value of the name property
myBook.name = "Testable JavaScript";
console.log(myBook.name);    // Book: Testable JavaScript

Dieser Code verwendet Object.defineProperty(), um Accessor-Eigenschaften zu definieren. Accessor-Eigenschaften enthalten keine Eigenschaften oder Methoden, aber sie definieren einen Getter, der aufgerufen wird, wenn die Eigenschaft gelesen wird, und einen Setter, der aufgerufen wird, wenn die Eigenschaft geschrieben wird.

Ein Getter soll einen Wert zurückgeben, während ein Setter den Wert, der der Eigenschaft zugewiesen wird, als Argument erhält. Der obige Konstruktor gibt eine Instanz zurück, deren name-Eigenschaft festgelegt oder geändert werden kann, aber nicht gelöscht werden kann. Wenn wir den Wert von name abrufen, fügt der Getter der Zeichenfolge Book: den Namen hinzu und gibt ihn zurück.

Objekt-Literal-Schreibweisen werden Konstruktoren vorgezogen

Die JavaScript-Sprache hat neun eingebaute Konstruktoren: Object(), Array(), String(), Number(), Boolean(), Date(), Function(), Error() und RegExp(). Beim Erstellen von Werten können wir entweder Objekt-Literale oder Konstruktoren verwenden. Objekt-Literale sind jedoch nicht nur leichter zu lesen, sondern auch schneller auszuführen, da sie zur Parse-Zeit optimiert werden können. Daher ist es am besten, sich bei einfachen Objekten an Literale zu halten

// a number object
// numbers have a toFixed() method
var obj = new Object(5);
obj.toFixed(2);     // 5.00

// we can achieve the same result using literals
var num = 5;
num.toFixed(2);     // 5.00

// a string object
// strings have a slice() method 
var obj = new String("text");
obj.slice(0,2);     // "te"

// same as above
var string = "text";
string.slice(0,2);  // "te"

Wie Sie sehen, gibt es kaum einen Unterschied zwischen Objekt-Literalen und Konstruktoren. Interessanter ist, dass es immer noch möglich ist, Methoden auf Literalen aufzurufen. Wenn eine Methode auf einem Literal aufgerufen wird, konvertiert JavaScript das Literal automatisch in ein temporäres Objekt, damit die Methode die Operation ausführen kann. Sobald das temporäre Objekt nicht mehr benötigt wird, verwirft JavaScript es.

Die Verwendung des new-Schlüsselworts ist unerlässlich

Es ist wichtig zu bedenken, dass vor allen Konstruktoren das Schlüsselwort new verwendet werden muss. Wenn Sie new versehentlich vergessen, ändern Sie das globale Objekt anstelle des neu erstellten Objekts. Betrachten Sie das folgende Beispiel

function Book(name, year) {
  console.log(this);
  this.name = name;
  this.year = year;
}

var myBook = Book("js book", 2014);  
console.log(myBook instanceof Book);  
console.log(window.name, window.year);

var myBook = new Book("js book", 2014);  
console.log(myBook instanceof Book);  
console.log(myBook.name, myBook.year);

Hier ist, was dieser Code auf der Konsole ausgibt

Wenn wir den Book-Konstruktor ohne new aufrufen, rufen wir tatsächlich eine Funktion ohne Rückgabeanweisung auf. Daher verweist this innerhalb des Konstruktors auf Window (anstelle von myBook), und es werden zwei globale Variablen erstellt. Wenn wir die Funktion jedoch mit new aufrufen, wird der Kontext von global (Window) auf die Instanz umgeschaltet. Daher verweist this korrekt auf myBook.

Beachten Sie, dass dieser Code im Strict Mode einen Fehler auslösen würde, da der Strict Mode dazu dient, den Programmierer vor versehentlichem Aufrufen eines Konstruktors ohne das Schlüsselwort new zu schützen.

Scope-sichere Konstruktoren

Wie wir gesehen haben, ist ein Konstruktor einfach eine Funktion, daher kann er ohne das Schlüsselwort new aufgerufen werden. Aber für unerfahrene Programmierer kann dies eine Fehlerquelle sein. Ein Scope-sicherer Konstruktor ist so konzipiert, dass er dasselbe Ergebnis liefert, unabhängig davon, ob er mit oder ohne new aufgerufen wird, und leidet daher nicht unter diesen Problemen.

Die meisten eingebauten Konstruktoren wie Object, Regex und Array sind Scope-sicher. Sie verwenden ein spezielles Muster, um zu bestimmen, wie der Konstruktor aufgerufen wird. Wenn new nicht verwendet wird, geben sie eine ordnungsgemäße Instanz des Objekts zurück, indem sie den Konstruktor erneut mit new aufrufen. Betrachten Sie den folgenden Code

function Fn(argument) { 

  // if "this" is not an instance of the constructor
  // it means it was called without new  
  if (!(this instanceof Fn)) { 

    // call the constructor again with new
    return new Fn(argument);
  } 
}

Eine Scope-sichere Version unseres Konstruktors würde also so aussehen

function Book(name, year) { 
  if (!(this instanceof Book)) { 
    return new Book(name, year);
  }
  this.name = name;
  this.year = year;
}

var person1 = new Book("js book", 2014);
var person2 = Book("js book", 2014);

console.log(person1 instanceof Book);    // true
console.log(person2 instanceof Book);    // true

Fazit

Es ist wichtig zu verstehen, dass die Klassen Deklaration, die in ES2015 eingeführt wurde, einfach als syntaktischer Zucker über der bestehenden Prototypen-basierten Vererbung fungiert und nichts Neues zu JavaScript hinzufügt. Konstruktoren und Prototypen sind die primäre Methode von JavaScript, um ähnliche und verwandte Objekte zu definieren.

In diesem Artikel haben wir uns genau angesehen, wie JavaScript-Konstruktoren funktionieren. Wir haben gelernt, dass Konstruktoren wie normale Funktionen sind, aber mit dem Schlüsselwort new verwendet werden. Wir haben gesehen, wie Konstruktoren es uns ermöglichen, schnell mehrere ähnliche Objekte mit denselben Eigenschaften und Methoden zu erstellen, und warum der instanceof-Operator der sicherste Weg ist, den Typ einer Instanz zu bestimmen. Schließlich haben wir uns Scope-sichere Konstruktoren angesehen, die mit oder ohne new aufgerufen werden können.