Object.create()

Seit ES6 ist Object.create() immer seltener im Einsatz (kommt von ES5):

// Object-Erstellung auf Grundlage eines Prototypen

// Prototyp
let whiskey = {
    name:       "",
    cat:        ["Whiskey"],
    price:      0,
    quantity:   500,
    shortPrint () {
        console.log(`${this.cat[0]}: ${this.name} - ${this.price}`);
    },
    slogan(msg) {
        console.log(`${this.name} - ${msg}`);
    }
};

console.log(whiskey);
whiskey.shortPrint();

// Objekt auf Grundlage des Prototypen erstellen
let newProd = Object.create(whiskey);  // alle vererbten Eigenschaften unter 'prototype'
console.log(newProd);
console.log(newProd.price);
// eigene Daten definieren
//    wird direkt im Objekt hinzugefügt
newProd.name = "Shivas Regal";
newProd.price = 29.99;
//    Referenz-Objekt, daher wird Wert unter 'prototype' verändert
newProd.cat.unshift("Office Drinks");

console.log(newProd);
console.log(newProd.price);
newProd.shortPrint();

console.log(whiskey);   // Referenz-Objekt (cat) wurde AUCH HIER verändert, da Referenz-Objekt

Eigenschaftsattribute

Für Eigenschaften eines Objekts lassen sich Attribute definieren. Falls diese nicht gesetzt werden, gilt der Default.

/*
    zur Konfiguration von Object-Properties

    Attribut        Nutzung

    value           Wert einer Eigenschaft

    writable        gibt an, ob Eigenschaft überschreibbar ist
                    (nachträgliche Änderung des values)
                    default: false

    enumerable      gibt an, ob Eigenschaft iterierbar ist
                    (ob sie in Iteration geliefert wird)
                    default: false

    configurable    gibt an, ob Eigenschaft konfigurierbar ist
                    (ob sie bspw. gelöscht werden kann)
                    default: false

    set             definiert Funktion, mit der schreibend auf Eigenschaft
                    zugegriffen werden kann (nur für Zugriffseigenschaften)

    get             definiert Funktion, mit der lesend auf Eigenschaft
                    zugegriffen werden kann (nur für Zugriffseigenschaften)
*/

// Prototyp, der eine Methode beinhaltet

let heroActions = {
    getHeroDatas () {
        return `${this.name} (${this.age} wohnt in ${this.address})`;
    }
};

// Objekt auf Grundlage des Prototypen erstellen
let hero = Object.create(heroActions, {
    name:   {
        // initialer Wert der Eigenschaft (propertiesObject)
        value:          "Jessica",
        writable:       true,   // Eigenschaft ist überschreibbar
        enumerable:     true,   // Eigenschaft ist iterierbar
        configurable:   true    // Eigenschaft ist konfigurierbar
    },
    age:    {
        value:          36
    },
    address:    {
        value:          "Dragonroad",
        writable:       true
    }
});

hero.name = "Elektra";  // OK
// hero.age = 29;   // TypeError: "age" is read-only
// delete hero.age;   // TypeError: property "age" is non-configurable and can't be deleted
// delete hero.address;  // TypeError: property "address" is non-configurable and can't be deleted
hero.address = "Alexanderplatz 111, Berlin";  // OK
console.log(hero);  // object

for (let key in hero) {
    console.log(key);  // nur 'name' und 'getHeroDatas' (Rest nicht iterierbar)
    // prototype-Einträge werden standardmäßig ebenfalls berücksichtigt

    // nur EIGENE Eigenschaften berücksichtigen
    if (hero.hasOwnProperty(key)) {
        console.log(key);   // nur 'name'
    }
}

Neue Properties in Prototypen definieren

Object.prototype wird um weitere Attribute (hier: Funktionen) erweitert.

Important

Jedes Objekt in JavaScript besitzt eine eingebaute Eigenschaft, die prototype genannt wird. Dieser prototype ist selbst wieder ein Objekt, und hat daher ebenfalls einen eigenen prototype. So entsteht eine sogenannte Prototype-Kette (prototype chain).

Die Kette endet, sobald ein prototype erreicht wird, dessen eigener prototype den Wert null hat.

Prototypen erlauben die Vererbung von Properties in JavaScript. Wenn ein Property eines Objekts aufgerufen wird, wird entlang der Prototype-Kette nach diesem Property gesucht und das erste gefundene Match verwendet.

Object.prototype.print = function() {
    console.log("print-function", this);
}

// hero Objekt von oben
hero.print();

NodeList.prototype.giveBack = function() {
    console.log("giveBack hello");
};

document.querySelectorAll("li").giveBack();  // OK, da NodeList diese Methode hat
// document.querySelector("li").giveBack();   // NOK, da Node diese Methode nicht hat

// Event mit on() registrieren
Node.prototype.on = function (event, fn) {
    this.addEventListener(event, fn);
    return this;
};

// ... und mit off() de-registrieren
Node.prototype.off = function (event, fn) {
    this.removeEventListener(event, fn);
    return this;
};

let giveAlert = function() {
    alert("Es hat Klick gemacht");
};
document.querySelector("li").on("click", giveAlert);    // Event-Listener registrieren
document.querySelector("li").off("click", giveAlert);   // Event-Listener de-registrieren

Prototype einem Objekt zuordnen

Mittlerweile über die Object.setPrototypeOf() um einen Prototypen für ein Objekt zuzuweisen.

/*
    setzen / zuordnen eines Prototypen
    __proto__ (deprecated) || Object.setPrototypeOf()
*/

// Prototyp, der Methode beinhaltet
let heroActions = {
    getHeroDatas () {
        return `${this.name} (${this.age} wohnt in ${this.address})`;
    }
};
let heroDatas = {
    name:       "Jessica Jones",
    age:        36,
    address:    "Dragonraod 66"
};

// Object.setPrototypeOf(object, prototype);
// Füge 'heroActions' unter Prototypenn von 'heroDatas' hinzu
Object.setPrototypeOf(heroDatas, heroActions);

console.log(heroDatas.getHeroDatas());  // Jessica Jones (36 wohnt in Dragonraod 66)

Klassensyntax

JS macht kein “richtiges” OOP, es fühlt sich aber so an.

class Product {

    // constructor = Herzstück der Klasse
    // wird beim Instanziieren zuerst ausgeführt
    constructor(prodName, prodPrice, prodCat, prodQty) {
        this.name = prodName;
        this._price = prodPrice;  // '_' um Property nicht mit Setter zu verwechseln
        this.cat = prodCat;
        this.qty = prodQty;
    }

    // über 'get' wird Funktion als Getter gekennzeichnet und ist nun eine Eigenschaft
    get shortPrint() {
        return `${this.name}: ${this._price} EUR (${this.qty} Stück auf Lager)`;
    }
    // hier kann arrow-function verwendet werden, da Objekt bereits erzeugt ist
    // Funktion wird im Objekt selbst verankert
    logThis = () => console.log(this);

    // über 'set' wird Funktion als Setter gekennzeichnet und ist nun eine Eigenschaft
    // Eigenschaft/Funktion wird im 'prototype' verankert
    set addQty(qty) {
        this.qty += qty;
    }

    set subQty(qty) {
        (this.checkQty(qty)) ? this.qty -= qty : console.log("unzureichender Lagerbestand");
    }

    checkQty(qty) {
        return (this.qty - qty > 1);
    }

    get price() {
        return this._price;
    }

    set price(newVal) {
        this._price = newVal;
    }

};

let newProd = new Product("Talisker", 49.99, "Whiskey", 500);
console.log(newProd);
newProd.logThis();

// geht nicht mehr wegen 'set' Bezeichner ...
// newProd.subQty(250);
// newProd.addQty(100);
// newProd.subQty(500);
// ... muss nun als Eigenschaft verändert werden
newProd.subQty = 250;       // 500 - 250 -> 250
newProd.addQty = 100;       // 250 + 100 -> 150
newProd.subQty = 500;       // 350 - 500 -> -150: checkQty verhindert Abzug von 500 Stück
console.log(newProd);
console.log(newProd.shortPrint);    // Talisker: 49.99 EUR (350 Stück auf Lager)

/*
    Bezeichner für Getter und Setter müssen sich von Eigenschaften unterscheiden,
    damit es nicht zu Endlos-Rekursionen, Stack-Überlauf und letztlich
    Programmabsturz kommt

    oft werden Eigenschaften, auf die nicht direkt zugegriffen werden soll,
    mit '_' geprefixt - dann sind Zugriffsmethoden vorhanden!

    Jede Klasse in eigenem Skript verorten!
*/

Eigenschaftsattribute für Properties setzen

// Vorhandene Property
// -------------------

//     im Objekt sind alle schon vorhandenen Attribute standardmäßig überschreibbar,
//     sofern die Eigenschaftattribute nicht anders gesetzt wurden
Object.defineProperty(newProd, "name", {
        writable: false
    }
);
//    bei Erzeugung von Objekten werden Eigenschaftsattribute auf 'true' gesetzt,
//    wenn sie ohne Konfiguration der Eigenschaftsattribute erzeugt wurden

// newProd.name = "whatever";  // TypeError: "name" is read-only

// Neue Property
// -------------
Object.defineProperty(newProd, "desc", {
    value: "Best Quality Whiskey",
    enumerable: true
    // defineProperty: writable standardmäßig auf false -> read-only
});

// newProd.desc = "BliBlaBlub";  // TypeError: "desc" is read-only

for (let key in newProd) {
    console.log(key);
}

Arbeit mit Settern und Gettern der Eigenschaftsattribute

Über Object.defineProperty() wird eine neue Property an ein bereits vorhandene Instanz eines Objekts hinzugefügt:

let qualityVal = "Fine Best Strong...";
Object.defineProperty(newProd, "quality", {
    get() {
        return qualityVal;
    },
    set(newVal) {
        qualityVal = newVal;
    },
    enumerable: true
});

console.log(newProd);
newProd.quality = "Finest";
console.log(newProd.quality);   // Finest

Kind-Klasse erstellen (Klassen erweitern)

class LtdProduct extends Product {
    constructor (name, cat, price, qty, limit) {
        // immer zuerst super() aufrufen! (sonst Fehler)
        // danach neue / spezielleren Eigenschaften hinzufügen
        super(name, price, cat, qty);
        this.limit = limit;
    }
    // Methoden der Elternklasse stehen zur Verfügung
    // könnt diese nutzen, ändern oder überschreiben

    set addQty(qty) {
        (this.checkLimit(qty)) ? super.addQty = qty : console.log("Limit überschritten");
    }

    checkLimit(qty) {
        return !(this.qty + qty > this.limit);
    }
}

let ltdProduct = new LtdProduct("Talisker Limited Edition",
    "Home Drinks", 399.50, 250, 500);
console.log(ltdProduct);
ltdProduct.subQty = 59;
console.log(ltdProduct.shortPrint);
ltdProduct.addQty = 50;

Statische Objekt-Eigenschaften (Methoden & Properties)

Statische Eigenschaften und Methoden können nur auf der Klasse angewandt werden, nicht auf der Instanz dieser Klasse.

// Statische Methoden
// -> Anwendung nur auf Klasse, nicht auf Instanzen

class Product {

    constructor(prodName, prodPrice, prodCat, prodQty, prodArt) {
        this.name = prodName;
        this._price = prodPrice;
        this.cat = prodCat;
        this.qty = prodQty;
        this.art = prodArt;
    }

    get shortPrint() {
        return `${this.name}: ${this._price} EUR (${this.qty} Stück auf Lager)`;
    }

    logThis = () => console.log(this);

    set addQty(qty) {
        this.qty += qty;
    }

    set subQty(qty) {
        (this.checkQty(qty)) ? this.qty -= qty : console.log("unzureichender Lagerbestand");
    }

    checkQty(qty) {
        return (this.qty - qty > 1);
    }

    get price() {
        return this._price;
    }

    set price(newVal) {
        this._price = newVal;
    }

    // Statische Methode
    static getWhiskey() {
        return {
            SL:     "Scotch",
            BB:     "Bourbon",
            IRE:    "Irish"
        };
    };

    // Statische Eigenschaft innerhalb der Klasse definieren
    // Achtung: stets im SCREAMING_SNAKE_CASE schreiben
    static artObj = {
        SL:     "Scotch",
        BB:     "Bourbon",
        IRE:    "Irish"
    }

};

// Statische Eigenschaft außerhalb eines Objekts definieren  -> VERALTET!!
Product.WHISKEY_ART = {
    SL:     "Scotch",
    BB:     "Bourbon",
    IRE:    "Irish"
}

let newProd = new Product("Glennfiddich", 39.99, "Whiskey", 500, Product.getWhiskey().IRE);
let newProd2 = new Product("Glennfiddich", 39.99, "Whiskey", 500, Product.artObj.IRE);
let newProd3 = new Product("Glennfiddich", 39.99, "Whiskey", 500, Product.WHISKEY_ART.IRE);

console.log(newProd);
console.log(newProd2);
console.log(newProd3);

/*
    statische Methoden und Eigenschaften:
        - werden direkt auf Klasse aufgerufen
        - werden nicht vererbt => sind also auf Instanzebene nicht verfügbar
*/

console.log(newProd.artObj);        // undefined
console.log(newProd.WHISKEY_ART)    // undefined

Symbole in Klassen

Über Object.getOwnPropertySymbols() lassen sich alle Symbole eines Objekts zurückgeben:

const DOG_NAME = Symbol("dogname");

class Dog {
    constructor (name) {
        this[DOG_NAME] = name;  // muss über Property-Access definiert werden
    }

    get name () {
        return this[DOG_NAME];
    }

    set name(newVal) {
        this[DOG_NAME] = newVal;
    }
}

let myDog = new Dog("Snoopy");
myDog.name = "Leika";
console.log(myDog.name);    // Leika

console.log(Object.getOwnPropertySymbols(myDog));   // [ Symbol("dogname") ]
myDog[Object.getOwnPropertySymbols(myDog)[0]] = "Moja";
console.log(myDog.name);    // Moja

Seit ES10 ist es möglich mit # eine private Eigenschaft zu definieren:

// private Eigenschaften (beginnt mit '#')
// erst seit ES10 möglich
// -> Zugriff nur innerhalb der Klasse, nicht von außerhalb
class HomeAnimals {
    #anzahl;
    #initialCount;

    constructor(count = 0) {
        this.#initialCount = count;
        this.reset(count);  // --> #count = 0
    }

    reset(value = this.#initialCount) {
        this.#anzahl = value;
    }

    addAnimal (count = 1) {
        this.#anzahl += count;
    }

    subAnimal (count = 1) {
        this.#anzahl -= count;
    }

    get actCount() {
        return this.#anzahl;
    }
}

let myAnimals = new HomeAnimals();      // myAnimals.#count = 0
console.log(myAnimals);

myAnimals.addAnimal(3);     // myAnimals.#count = 3
myAnimals.addAnimal();      // myAnimals.#count = 4
myAnimals.reset(2);         // myAnimals.#count = 2
myAnimals.subAnimal();      // myAnimals.#count = 1

console.log(myAnimals.actCount);    // 1

console.log(myAnimals["#anzahl"]);    // undefined --> da private Eigenschaft
console.log(HomeAnimals["#anzahl"]);    // undefined --> da private Eigenschaft