Pascal Precht

JavaScript-Objekte erzeugen – wie man’s richtig macht

Wenn man sich ein bisschen mit JavaScript befasst hat, fällt einem schnell auf, dass es viele verschiedene Wege gibt, verschiedene Dinge zu tun. Dies gilt zum Beispiel für Arrays, Objekten und auch Funktionen. Eigentlich sind natürlich alle diese Datentypen Objekte in JavaScript. Es gibt drei Wege, Objekte in JavaScript zu erzeugen. Jeder dieser Wege bietet Vor- und Nachteile. Ich möchte euch zeigen, wie man es auf jeden Fall nicht tun sollte und natürlich auch, warum das Ganze so ist.

Der Objekt-Konstruktor

Der erste und stumpfsinnigste Weg ein Objekt zu erzeugen, ist mit Hilfe des Objekt-Konstruktors. Dieser Konstruktor bringt einige Kniffe mit sich, dazu komm Ich gleich. Hier erstmal ein Beispiel, wie die Verwendung eines Objekt-Konstruktors aussieht.

var Car = new Object();
Car.color = "#000000";

console.log(Car.constructor === Object);    // true
console.log(Car.color); // "#000000"

Damit zeige ich euch wahrscheinlich nichts Neues. Aber schauen wir uns den Code mal genauer an. Wir erzeugen also ein Objekt Car mit Hilfe des Objekt-Konstruktors. Das Ergebnis ist wie erwartet, wir erhalten ein Objekt und können dieses mit Eigenschaften und Methoden füllen. Würden wir an Stelle des Objekt-Konstruktors Objekt-Literale verwenden, erhalten wir das gleiche Ergebnis.

Objekt-Literale

var Car = {};    // leeres Objekt
Car.color = "#000000";

// Oder alles in einem Rutsch

var Car = {
    color: "#000000"
};

Mit diesem Code erhalten wir also das gleiche Ergebnis, wie mit der Verwendung des Objekt-Konstruktors. Aber welche Variante soll man denn nun verwenden? Ganz einfach. Vergleichen wir beide Lösungen, stellen wir schnell fest, dass das Schreiben von Objekt-Literalen weniger Arbeit in Anspruch nimmt – also weniger Code. Ein weiter Grund den Objekt-Konstruktor nicht zu verwenden ist folgender:

Man kann dem Objekt-Konstruktor einen Parameter mitgeben. Das Ergebnis einer Objektinstanziierung mit Verwendung dieser Parameter kann sehr verwirrend sein. Übergeben wir nämlich einen Parameter, delegiert JavaScript die Objekterzeugung direkt weiter an einen der primitiven Datentypen. Hier ein Code zur Verdeutlichung.

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

var o = new Object(2);
console.log(o.constructor === Number);   // true

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

Ouh. Habt ihr das gewusst? Also. Wenn es keinen Grund gibt, den Objekt-Konstruktor zu verwenden, verzichtet drauf. Verwendet lieber Objekt-Literale. Hier gibt es keine hässlichen Seiteneffekte und schlankeren Code hat man auch noch dazu.

Die Konstruktor-Funktion

Der dritte Weg ein Objekt zu erzeugen, wäre eine selbst implementierte Konstruktor-Funktion. In so einem Fall macht man nichts weiter, als eine Funktion zu definieren, welche irgendwas macht. Anschließend kann diese Funktion als Konstruktor-Funktion verwendet werden, in dem man den new-Operator benutzt, um ein Objekt zu erzeugen. Und auch hier gibt es einige Kniffe. Schaut selbst.

function Car(manufacturer) {
    this.manufacturer = manufacturer;
    this.engineStarted = false;

    this.startEngine = function() {
        if (this.engineStarted === false) {
            this.engineStarted = true;
        }
    };
}

var bmw = new Car('BMW');
bmw.startEngine();

Nun haben wir einen simplen Konstruktor um Autos zu erzeugen. Einen Haken hat dieses Muster allerdings noch. Angenommen, ein weiterer Entwickler verwendet euren Code und möchte ebenfalls Autos erzeugen, weiß allerdings nicht, dass er den new-Operator verwenden muss. Momentmal… “verwenden MUSS”?! Yepp. Wird die Konstruktor-Funktion ohne new-Operator aufgerufen, befindet sich unser Konstruktor nicht im Objekt-Kontext und ist nichts weiter, als eine normale Funktion. Was wiederum bedeutet, das this keine Referenz auf unser Auto ist, sondern auf das globale Objekt zeigt. Keine Sorge. Auch für diesen Fall gibt es natürlich eine schöne Lösung.

Das “new” erzwingen

Mit dem Erzwingen ist an dieser Stelle aber nicht gemeint, den Benutzer eures Codes zu zwingen den new-Operator zu verwenden und er andernfalls mächtigen Ärger bekommt, sondern viel mehr, die interne Nutzung von new ohne es explizit aufrufen zu müssen bzw. auf jeden Fall unser das entsprechende Objekt zurück zugeben. Das Ergebnis soll also so aussehen, dass es möglich ist, unsere Konstruktor-Funktion entweder mit, oder ohne new aufzurufen, man aber trotzdem das gleiche Ergebnis erhält. Nämlich unser Objekt.

Um das in die Wege zu leiten, gibt es mehrere Möglichkeiten. Die erste Möglichkeit wäre die Verwendung von that. Anstatt also innerhalb unseres Konstruktors auf this zu referenzieren, erzeugen wir ein leeres Objekt mit den Objekt-Literalen, füllen dies mit Eigenschaften und Methoden und geben dieses auch zurück. Somit bekommen wir in jedem fall unser Objekt.

function Car(manufacturer) {
    var that = {};

    that.manufacturer = manufacturer;
    that.engineStarted = false;

    that.startEngine = function() {
        if (that.engineStarted === false) {
            that.engineStarted = true;
        }
    };

    return that;
}

var bmw = new Car('BMW');
bmw.startEngine();

var vw = Car('VW');
vw.startEngine();

that ist in diesem Fall ein eingespielter Bezeichner. Man könnte auch einen anderen Namen verwenden. Wie man sieht, bekommen wir unser Auto-Objekt, selbst wenn wir unsere Konstruktor-Funktion nicht mit dem new-Operator aufrufen.

Die zweite Variante wäre einfach ein anonymes Objekt zurück zugeben. Hat ein bisschen was vom Modul-Pattern und ist im Großen und Ganzen eine etwas kürzere Variante des obigen Codes.

function Car(manufacturer) {
    return {
        manufacturer: manufacturer,
        engineStarted: false,
        startEngine: function () {
            if (this.engineStarted === false) {
                this.engineStarted = true;
            }
        }
    };
}

Easy hah? Nur gibt es – wie immer – bei diesen beiden Lösungswegen ein Problem. Da wir nicht this zurück geben (was wir auch nicht explizit tun müssen, da JavaScript in einem Objekt-Kontext ganz von allein this zurück gibt), sondern einfach ein weiteres Objekt zurück geben, um dieses auf jeden Fall zu erhalten, geht der Prototype unseres Konstruktors verloren. Also brauchen wir eine Lösung, in der wir sicher stellen, das unser Konstruktor in jedem Fall mit new aufgerufen wird. Klingt ein bisschen verdreht, ist aber gar nicht so schwer. Wir müssen nicht weiter tun, als zu prüfen, ob es sich bei this im aktuellen Kontext um eine Referenz auf unser Auto-Objekt handelt. Ist dies nicht der Fall, müssen wir es eben explizit mit new erzwingen.

function Car() {

    if (!(this instance of Car)) {
        return new Car();
    }

    this.prop = "this is a property";
}

Car.prototype.vehicle = "Bus";

var car = new Car();
var second_car = Car();

console.log(car.prop); // "this is a property"
console.log(second_car.prop); // "this is a property"

console.log(car.vehicle); // "Bus"
console.log(second_car.vehicle); // "Bus"