14. Neue OOP-Features neben Klassen
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

14. Neue OOP-Features neben Klassen

Klassen (die im nächsten Kapitel erklärt werden) sind das wichtigste neue OOP-Feature in ECMAScript 6. Darüber hinaus enthält es neue Features für Objekt-Literale und neue Hilfsmethoden in Object. Dieses Kapitel beschreibt sie.



14.1 Übersicht

14.1.1 Neue Features für Objekt-Literale

Methodendefinitionen

const obj = {
    myMethod(x, y) {
        ···
    }
};

Kurzschreibweise für Eigenschaftswerte

const first = 'Jane';
const last = 'Doe';

const obj = { first, last };
// Same as:
const obj = { first: first, last: last };

Berechenbare Eigenschaftsschlüssel

const propKey = 'foo';
const obj = {
    [propKey]: true,
    ['b'+'ar']: 123
};

Diese neue Syntax kann auch für Methodendefinitionen verwendet werden

const obj = {
    ['h'+'ello']() {
        return 'hi';
    }
};
console.log(obj.hello()); // hi

Der Hauptanwendungsfall für berechenbare Eigenschaftsschlüssel ist die einfache Verwendung von Symbolen als Eigenschaftsschlüssel.

14.1.2 Neue Methoden in Object

Die wichtigste neue Methode von Object ist assign(). Traditionell wurde diese Funktionalität in der JavaScript-Welt extend() genannt. Im Gegensatz zur Funktionsweise dieser klassischen Operation berücksichtigt Object.assign() nur eigene (nicht geerbte) Eigenschaften.

const obj = { foo: 123 };
Object.assign(obj, { bar: true });
console.log(JSON.stringify(obj));
    // {"foo":123,"bar":true}

14.2 Neue Features von Objekt-Literalen

14.2.1 Methodendefinitionen

In ECMAScript 5 sind Methoden Eigenschaften, deren Werte Funktionen sind

var obj = {
    myMethod: function (x, y) {
        ···
    }
};

In ECMAScript 6 sind Methoden immer noch funktionsbezogene Eigenschaften, aber es gibt jetzt eine kompaktere Möglichkeit, sie zu definieren

const obj = {
    myMethod(x, y) {
        ···
    }
};

Getter und Setter funktionieren weiterhin wie in ECMAScript 5 (beachten Sie, wie syntaktisch ähnlich sie den Methodendefinitionen sind)

const obj = {
    get foo() {
        console.log('GET foo');
        return 123;
    },
    set bar(value) {
        console.log('SET bar to '+value);
        // return value is ignored
    }
};

Verwenden wir obj

> obj.foo
GET foo
123
> obj.bar = true
SET bar to true
true

Es gibt auch eine Möglichkeit, Eigenschaften prägnant zu definieren, deren Werte Generatorfunktionen sind

const obj = {
    * myGeneratorMethod() {
        ···
    }
};

Dieser Code ist äquivalent zu

const obj = {
    myGeneratorMethod: function* () {
        ···
    }
};

14.2.2 Kurzschreibweise für Eigenschaftswerte

Die Kurzschreibweise für Eigenschaftswerte ermöglicht Ihnen die Abkürzung der Definition einer Eigenschaft in einem Objekt-Literal: Wenn der Name der Variablen, die den Eigenschaftswert angibt, auch der Eigenschaftsschlüssel ist, können Sie den Schlüssel weglassen. Dies sieht wie folgt aus.

const x = 4;
const y = 1;
const obj = { x, y };

Die letzte Zeile ist äquivalent zu

const obj = { x: x, y: y };

Die Kurzschreibweise für Eigenschaftswerte funktioniert gut zusammen mit der Destrukturierung

const obj = { x: 4, y: 1 };
const {x,y} = obj;
console.log(x); // 4
console.log(y); // 1

Ein Anwendungsfall für die Kurzschreibweise von Eigenschaftswerten sind mehrere Rückgabewerte (die im Kapitel über Destrukturierung erklärt werden).

14.2.3 Berechenbare Eigenschaftsschlüssel

Denken Sie daran, dass es beim Setzen einer Eigenschaft zwei Möglichkeiten gibt, einen Schlüssel anzugeben.

  1. Über einen festen Namen: obj.foo = true;
  2. Über einen Ausdruck: obj['b'+'ar'] = 123;

In Objekt-Literalen haben Sie in ECMAScript 5 nur Option #1. ECMAScript 6 bietet zusätzlich Option #2 (Zeile A)

const obj = {
    foo: true,
    ['b'+'ar']: 123
};

Diese neue Syntax kann auch für Methodendefinitionen verwendet werden

const obj = {
    ['h'+'ello']() {
        return 'hi';
    }
};
console.log(obj.hello()); // hi

Der Hauptanwendungsfall für berechenbare Eigenschaftsschlüssel sind Symbole: Sie können ein öffentliches Symbol definieren und es als einen speziellen Eigenschaftsschlüssel verwenden, der immer eindeutig ist. Ein herausragendes Beispiel ist das in Symbol.iterator gespeicherte Symbol. Wenn ein Objekt eine Methode mit diesem Schlüssel hat, wird es iterierbar: Die Methode muss einen Iterator zurückgeben, der von Konstrukten wie der for-of-Schleife verwendet wird, um über das Objekt zu iterieren. Der folgende Code demonstriert, wie das funktioniert.

const obj = {
    * [Symbol.iterator]() { // (A)
        yield 'hello';
        yield 'world';
    }
};
for (const x of obj) {
    console.log(x);
}
// Output:
// hello
// world

obj ist iterierbar, aufgrund der Generator-Methodendefinition ab Zeile A.

14.3 Neue Methoden von Object

14.3.1 Object.assign(target, source_1, source_2, ···)

Diese Methode fasst die Quellen im Ziel zusammen: Sie modifiziert target, indem sie zuerst alle aufzählbaren eigenen (nicht geerbten) Eigenschaften von source_1 hineinkopiert, dann alle eigenen Eigenschaften von source_2 usw. Am Ende gibt sie das Ziel zurück.

const obj = { foo: 123 };
Object.assign(obj, { bar: true });
console.log(JSON.stringify(obj));
    // {"foo":123,"bar":true}

Betrachten wir genauer, wie Object.assign() funktioniert

14.3.1.1 Kopieren aller eigenen Eigenschaften

So würden Sie alle eigenen Eigenschaften (nicht nur aufzählbare) kopieren, während Getter und Setter korrekt übertragen werden und ohne Setter auf dem Ziel aufzurufen

function copyAllOwnProperties(target, ...sources) {
    for (const source of sources) {
        for (const key of Reflect.ownKeys(source)) {
            const desc = Object.getOwnPropertyDescriptor(source, key);
            Object.defineProperty(target, key, desc);
        }
    }
    return target;
}

Konsultieren Sie Abschn. „Eigenschaftsattribute und Eigenschaftsdeskriptoren“ in „Speaking JavaScript“ für weitere Informationen zu Eigenschaftsdeskriptoren (wie sie von Object.getOwnPropertyDescriptor() und Object.defineProperty() verwendet werden).

14.3.1.2 Vorsicht: Object.assign() funktioniert schlecht zum Verschieben von Methoden

Einerseits können Sie keine Methode verschieben, die super verwendet: Eine solche Methode hat den internen Slot [[HomeObject]], der sie an das Objekt bindet, in dem sie erstellt wurde. Wenn Sie sie über Object.assign() verschieben, verweist sie weiterhin auf die Super-Eigenschaften des ursprünglichen Objekts. Details werden in einem Abschnitt im Kapitel über Klassen erklärt.

Andererseits ist die Aufzählbarkeit falsch, wenn Sie Methoden, die von einem Objekt-Literal erstellt wurden, in den Prototyp einer Klasse verschieben. Die erstgenannten Methoden sind alle aufzählbar (andernfalls würde Object.assign() sie ohnehin nicht sehen), aber der Prototyp hat normalerweise nur nicht-aufzählbare Methoden.

14.3.1.3 Anwendungsfälle für Object.assign()

Betrachten wir einige Anwendungsfälle.

14.3.1.3.1 Hinzufügen von Eigenschaften zu this

Sie können Object.assign() verwenden, um Eigenschaften zu this in einem Konstruktor hinzuzufügen

class Point {
    constructor(x, y) {
        Object.assign(this, {x, y});
    }
}
14.3.1.3.2 Bereitstellen von Standardwerten für Objekteigenschaften

Object.assign() ist auch nützlich zum Auffüllen von Standardwerten für fehlende Eigenschaften. Im folgenden Beispiel haben wir ein Objekt DEFAULTS mit Standardwerten für Eigenschaften und ein Objekt options mit Daten.

const DEFAULTS = {
    logLevel: 0,
    outputFormat: 'html'
};
function processContent(options) {
    options = Object.assign({}, DEFAULTS, options); // (A)
    ···
}

In Zeile A haben wir ein neues Objekt erstellt, die Standardwerte hineinkopiert und dann options hineinkopiert, wodurch die Standardwerte überschrieben wurden. Object.assign() gibt das Ergebnis dieser Operationen zurück, das wir options zuweisen.

14.3.1.3.3 Hinzufügen von Methoden zu Objekten

Ein weiterer Anwendungsfall ist das Hinzufügen von Methoden zu Objekten

Object.assign(SomeClass.prototype, {
    someMethod(arg1, arg2) {
        ···
    },
    anotherMethod() {
        ···
    }
});

Sie können auch manuell Funktionen zuweisen, aber dann haben Sie nicht die nette Methodendefinitionssyntax und müssen jedes Mal SomeClass.prototype erwähnen

SomeClass.prototype.someMethod = function (arg1, arg2) {
    ···
};
SomeClass.prototype.anotherMethod = function () {
    ···
};
14.3.1.3.4 Klonen von Objekten

Ein letzter Anwendungsfall für Object.assign() ist eine schnelle Möglichkeit, Objekte zu klonen

function clone(orig) {
    return Object.assign({}, orig);
}

Diese Art des Klonens ist auch etwas schmutzig, da sie die Eigenschaftsattribute von orig nicht beibehält. Wenn das das ist, was Sie brauchen, müssen Sie Eigenschaftsdeskriptoren verwenden, so wie wir es implementiert haben, um copyAllOwnProperties().

Wenn Sie möchten, dass der Klon denselben Prototyp wie das Original hat, können Sie Object.getPrototypeOf() und Object.create() verwenden

function clone(orig) {
    const origProto = Object.getPrototypeOf(orig);
    return Object.assign(Object.create(origProto), orig);
}

14.3.2 Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols(obj) ruft alle eigenen (nicht geerbten) Symbol-wertigen Eigenschaftsschlüssel von obj ab. Es ergänzt Object.getOwnPropertyNames(), das alle Zeichenketten-wertigen eigenen Eigenschaftsschlüssel abruft. Konsultieren Sie einen späteren Abschnitt für weitere Details zum Durchlaufen von Eigenschaften.

14.3.3 Object.is(value1, value2)

Der strikte Gleichheitsoperator (===) behandelt zwei Werte anders, als man es erwarten würde.

Erstens ist NaN nicht gleich sich selbst.

> NaN === NaN
false

Das ist unglücklich, da es uns oft daran hindert, NaN zu erkennen

> [0,NaN,2].indexOf(NaN)
-1

Zweitens hat JavaScript zwei Nullen, aber die strikte Gleichheit behandelt sie, als wären sie derselbe Wert

> -0 === +0
true

Dies zu tun ist normalerweise eine gute Sache.

Object.is() bietet eine Möglichkeit, Werte zu vergleichen, die etwas genauer ist als ===. Es funktioniert wie folgt

> Object.is(NaN, NaN)
true
> Object.is(-0, +0)
false

Alles andere wird wie mit === verglichen.

14.3.3.1 Verwenden von Object.is() zum Finden von Array-Elementen

In der folgenden Funktion myIndexOf() kombinieren wir Object.is() mit der neuen ES6-Array-Methode findIndex(), um NaN in Arrays zu finden.

function myIndexOf(arr, elem) {
    return arr.findIndex(x => Object.is(x, elem));
}

const myArray = [0,NaN,2];
myIndexOf(myArray, NaN); // 1
myArray.indexOf(NaN); // -1

Wie Sie in der letzten Zeile sehen können, findet indexOf() NaN nicht.

14.3.4 Object.setPrototypeOf(obj, proto)

Diese Methode setzt den Prototyp von obj auf proto. Die nicht standardmäßige Methode hierfür in ECMAScript 5, die von vielen Engines unterstützt wird, ist die Zuweisung an die spezielle Eigenschaft __proto__. Die empfohlene Methode zum Setzen des Prototyps bleibt die gleiche wie in ECMAScript 5: während der Erstellung eines Objekts, über Object.create(). Das wird immer schneller sein als zuerst ein Objekt zu erstellen und dann seinen Prototyp zu setzen. Offensichtlich funktioniert es nicht, wenn Sie den Prototyp eines bestehenden Objekts ändern möchten.

14.4 Durchlaufen von Eigenschaften in ES6

14.4.1 Fünf Operationen, die Eigenschaften durchlaufen

In ECMAScript 6 kann der Schlüssel einer Eigenschaft entweder eine Zeichenkette oder ein Symbol sein. Die folgenden fünf Operationen durchlaufen die Eigenschaftsschlüssel eines Objekts obj

14.4.2 Reihenfolge des Durchlaufens von Eigenschaften

ES6 definiert zwei Durchlauf-Reihenfolgen für Eigenschaften.

Eigene Eigenschaftsschlüssel

Aufzählbare eigene Namen

Die Reihenfolge, in der for-in Eigenschaften durchläuft, ist nicht definiert. Zitiert von Allen Wirfs-Brock

Historisch gesehen war die for-in-Reihenfolge nicht definiert und es gab Unterschiede zwischen den Browser-Implementierungen in der Reihenfolge, die sie produzierten (und andere Besonderheiten). ES5 fügte Object.keys hinzu und die Anforderung, dass es die Schlüssel identisch zu for-in ordnen sollte. Während der Entwicklung von ES5 und ES6 wurde die Möglichkeit, eine spezifische for-in-Reihenfolge zu definieren, in Betracht gezogen, aber aufgrund von Kompatibilitätsproblemen mit der Web-Legacy und Unsicherheiten über die Bereitschaft der Browser, Änderungen an der von ihnen aktuell produzierten Reihenfolge vorzunehmen, nicht übernommen.

14.4.2.1 Ganzzahlige Indizes

Auch wenn Sie auf Array-Elemente über ganzzahlige Indizes zugreifen, behandelt die Spezifikation sie als normale Zeichenketten-Eigenschaftsschlüssel

const arr=['a', 'b', 'c'];

console.log(arr['0']); // 'a'

// Operand 0 of [] is coerced to string:
console.log(arr[0]); // 'a'

Ganzzahlige Indizes sind nur in zwei Fällen besonders: sie beeinflussen die length eines Arrays und sie kommen zuerst, wenn Eigenschaftsschlüssel aufgelistet werden.

Grob gesagt ist ein ganzzahliger Index eine Zeichenkette, die, wenn sie in einen 53-Bit nicht-negativen Integer konvertiert und zurück konvertiert wird, denselben Wert hat. Daher

Weiterführende Lektüre

14.4.2.2 Beispiel

Der folgende Code demonstriert die Durchlauf-Reihenfolge „Eigene Eigenschaftsschlüssel“

const obj = {
    [Symbol('first')]: true,
    '02': true,
    '10': true,
    '01': true,
    '2': true,
    [Symbol('second')]: true,
};
Reflect.ownKeys(obj);
    // [ '2', '10', '02', '01',
    //   Symbol('first'), Symbol('second') ]

Erklärung

14.4.2.3 Warum standardisiert die Spezifikation die Reihenfolge, in der Eigenschaftsschlüssel zurückgegeben werden?

Antwort von Tab Atkins Jr.

Weil bei Objekten zumindest alle Implementierungen ungefähr dieselbe Reihenfolge verwendeten (passend zur aktuellen Spezifikation), und viel Code unabsichtlich geschrieben wurde, der von dieser Reihenfolge abhing und brechen würde, wenn man ihn in einer anderen Reihenfolge aufzählen würde. Da Browser diese spezifische Reihenfolge aus Gründen der Web-Kompatibilität implementieren müssen, wurde sie als Anforderung in die Spezifikation aufgenommen.

Es gab einige Diskussionen darüber, davon bei Maps/Sets abzuweichen, aber das würde erfordern, eine Reihenfolge zu spezifizieren, auf die der Code *unmöglich* angewiesen sein kann; mit anderen Worten, wir müssten vorschreiben, dass die Reihenfolge zufällig ist, nicht nur undefiniert. Dies wurde als zu viel Aufwand angesehen, und die Erstellungsreihenfolge ist von relativ hohem Wert (siehe z.B. OrderedDict in Python), daher wurde beschlossen, dass Maps und Sets mit Objekten übereinstimmen.

14.4.2.4 Die Reihenfolge der Eigenschaften in der Spezifikation

Die folgenden Teile der Spezifikation sind für diesen Abschnitt relevant

14.5 Zuweisen vs. Definieren von Eigenschaften

Es gibt zwei ähnliche Möglichkeiten, eine Eigenschaft prop zu einem Objekt obj hinzuzufügen

Es gibt drei Fälle, in denen das Zuweisen keine eigene Eigenschaft prop erstellt – auch wenn sie noch nicht existiert

  1. Eine schreibgeschützte Eigenschaft prop existiert in der Prototypenkette. Dann verursacht die Zuweisung im strikten Modus einen TypeError.
  2. Ein Setter für prop existiert in der Prototypenkette. Dann wird dieser Setter aufgerufen.
  3. Ein Getter für prop ohne Setter existiert in der Prototypenkette. Dann wird im strikten Modus ein TypeError ausgelöst. Dieser Fall ist ähnlich dem ersten.

Keiner dieser Fälle verhindert, dass Object.defineProperty() eine eigene Eigenschaft erstellt. Der nächste Abschnitt behandelt Fall #3 genauer.

14.5.1 Überschreiben von geerbten schreibgeschützten Eigenschaften

Wenn ein Objekt obj eine schreibgeschützte Eigenschaft prop erbt, können Sie dieser Eigenschaft nichts zuweisen

const proto = Object.defineProperty({}, 'prop', {
    writable: false,
    configurable: true,
    value: 123,
});
const obj = Object.create(proto);
obj.prop = 456;
    // TypeError: Cannot assign to read-only property

Dies ist ähnlich wie bei einer geerbten Eigenschaft, die einen Getter, aber keinen Setter hat. Es steht im Einklang mit der Betrachtung der Zuweisung als Änderung des Werts einer geerbten Eigenschaft. Sie tut dies nicht-destruktiv: das Original wird nicht verändert, sondern durch eine neu erstellte eigene Eigenschaft überschrieben. Daher verhindern eine geerbte schreibgeschützte Eigenschaft und eine geerbte Eigenschaft ohne Setter Änderungen durch Zuweisung. Sie können jedoch die Erstellung einer eigenen Eigenschaft erzwingen, indem Sie eine Eigenschaft definieren

const proto = Object.defineProperty({}, 'prop', {
    writable: false,
    configurable: true,
    value: 123,
});
const obj = Object.create(proto);
Object.defineProperty(obj, 'prop', {value: 456});
console.log(obj.prop); // 456

14.6 __proto__ in ECMAScript 6

Die Eigenschaft __proto__ (ausgesprochen „dunder proto“) existiert schon seit einiger Zeit in den meisten JavaScript-Engines. Dieser Abschnitt erklärt, wie sie vor ECMAScript 6 funktionierte und was sich mit ECMAScript 6 ändert.

Für diesen Abschnitt ist es hilfreich, wenn Sie wissen, was Prototypenkette ist. Konsultieren Sie Abschn. „Ebene 2: Die Prototyp-Beziehung zwischen Objekten“ in „Speaking JavaScript“, falls erforderlich.

14.6.1 __proto__ vor ECMAScript 6

14.6.1.1 Prototypen

Jedes Objekt in JavaScript beginnt eine Kette von einem oder mehreren Objekten, eine sogenannte Prototypenkette. Jedes Objekt verweist über den internen Slot [[Prototype]] auf sein Nachfolgestück, seinen Prototyp (der null ist, wenn kein Nachfolger vorhanden ist). Dieser Slot wird als intern bezeichnet, da er nur in der Sprachspezifikation existiert und nicht direkt aus JavaScript aufgerufen werden kann. In ECMAScript 5 ist der Standardweg, um den Prototyp p eines Objekts obj zu erhalten

var p = Object.getPrototypeOf(obj);

Es gibt keine Standardmethode, um den Prototyp eines bestehenden Objekts zu ändern, aber Sie können ein neues Objekt obj erstellen, das den gegebenen Prototyp p hat

var obj = Object.create(p);
14.6.1.2 __proto__

Vor langer Zeit erhielt Firefox die nicht standardmäßige Eigenschaft __proto__. Andere Browser kopierten diese Funktion schließlich aufgrund ihrer Beliebtheit.

Vor ECMAScript 6 funktionierte __proto__ auf obskure Weise

14.6.1.3 Unterklassenbildung von Array über __proto__

Der Hauptgrund, warum __proto__ populär wurde, war, dass sie den einzigen Weg ermöglichte, eine Unterklasse MyArray von Array in ES5 zu erstellen: Array-Instanzen waren exotische Objekte, die nicht mit gewöhnlichen Konstruktoren erstellt werden konnten. Daher wurde der folgende Trick verwendet

function MyArray() {
    var instance = new Array(); // exotic object
    instance.__proto__ = MyArray.prototype;
    return instance;
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.customMethod = function (···) { ··· };

Die Unterklassenbildung in ES6 funktioniert anders als in ES5 und unterstützt die Unterklassenbildung von Built-ins direkt.

14.6.1.4 Warum ist __proto__ in ES5 problematisch?

Das Hauptproblem ist, dass __proto__ zwei Ebenen mischt: die Objektebene (normale Eigenschaften, die Daten halten) und die Meta-Ebene.

Wenn Sie versehentlich __proto__ als normale Eigenschaft (Objektebene!) verwenden, um Daten zu speichern, geraten Sie in Schwierigkeiten, da die beiden Ebenen kollidieren. Die Situation wird dadurch verschärft, dass Sie Objekte in ES5 als Maps missbrauchen müssen, da es keine eingebaute Datenstruktur dafür gibt. Maps sollten beliebige Schlüssel aufnehmen können, aber Sie können den Schlüssel '__proto__' mit Objekten als Maps nicht verwenden.

Theoretisch könnte man das Problem durch die Verwendung eines Symbols anstelle des speziellen Namens __proto__ beheben, aber die vollständige Trennung von Meta-Mechanismen (wie über Object.getPrototypeOf()) ist der beste Ansatz.

14.6.2 Die beiden Arten von __proto__ in ECMAScript 6

Da __proto__ so weit verbreitet unterstützt wurde, wurde beschlossen, sein Verhalten für ECMAScript 6 zu standardisieren. Aufgrund seiner problematischen Natur wurde es jedoch als veraltete Funktion hinzugefügt. Diese Funktionen befinden sich in Annex B der ECMAScript-Spezifikation, der wie folgt beschrieben wird

Die in diesem Anhang definierten ECMAScript-Sprachsyntax und Semantik sind erforderlich, wenn der ECMAScript-Host ein Webbrowser ist. Der Inhalt dieses Anhangs ist normativ, aber optional, wenn der ECMAScript-Host kein Webbrowser ist.

JavaScript hat mehrere unerwünschte Funktionen, die von einer erheblichen Menge an Code im Web benötigt werden. Daher müssen Webbrowser sie implementieren, andere JavaScript-Engines müssen es jedoch nicht.

Um die Magie hinter __proto__ zu erklären, wurden in ES6 zwei Mechanismen eingeführt

14.6.2.1 Object.prototype.__proto__

ECMAScript 6 ermöglicht das Abrufen und Setzen der Eigenschaft __proto__ über einen Getter und einen Setter, die in Object.prototype gespeichert sind. Wenn Sie sie manuell implementieren würden, sähe es ungefähr so aus

Object.defineProperty(Object.prototype, '__proto__', {
    get() {
        const _thisObj = Object(this);
        return Object.getPrototypeOf(_thisObj);
    },
    set(proto) {
        if (this === undefined || this === null) {
            throw new TypeError();
        }
        if (!isObject(this)) {
            return undefined;
        }
        if (!isObject(proto)) {
            return undefined;
        }
        const status = Reflect.setPrototypeOf(this, proto);
        if (! status) {
            throw new TypeError();
        }
        return undefined;
    },
});
function isObject(value) {
    return Object(value) === value;
}
14.6.2.2 Der Eigenschaftsschlüssel __proto__ als Operator in einem Objekt-Literal

Wenn __proto__ als unquoted oder quoted Eigenschaftsschlüssel in einem Objekt-Literal erscheint, wird der Prototyp des von diesem Literal erstellten Objekts auf den Eigenschaftswert gesetzt

> Object.getPrototypeOf({ __proto__: null })
null
> Object.getPrototypeOf({ '__proto__': null })
null

Die Verwendung des Zeichenkettenwerts '__proto__' als berechenbarer Eigenschaftsschlüssel ändert den Prototyp nicht, sondern erstellt eine eigene Eigenschaft

> const obj = { ['__proto__']: null };
> Object.getPrototypeOf(obj) === Object.prototype
true
> Object.keys(obj)
[ '__proto__' ]

14.6.3 Vermeiden des Magischen von __proto__

14.6.3.1 Definieren (nicht Zuweisen) von __proto__

In ECMAScript 6 wird, wenn Sie die eigene Eigenschaft __proto__ definieren, keine spezielle Funktionalität ausgelöst und der Getter/Setter Object.prototype.__proto__ wird überschrieben

const obj = {};
Object.defineProperty(obj, '__proto__', { value: 123 })

Object.keys(obj); // [ '__proto__' ]
console.log(obj.__proto__); // 123
14.6.3.2 Objekte, die Object.prototype nicht als Prototyp haben

Der __proto__ Getter/Setter wird über Object.prototype bereitgestellt. Daher hat ein Objekt ohne Object.prototype in seiner Prototypenkette auch keinen Getter/Setter. Im folgenden Code ist dict ein Beispiel für ein solches Objekt – es hat keinen Prototyp. Infolgedessen funktioniert __proto__ jetzt wie jede andere Eigenschaft

> const dict = Object.create(null);
> '__proto__' in dict
false
> dict.__proto__ = 'abc';
> dict.__proto__
'abc'
14.6.3.3 __proto__ und dict-Objekte

Wenn Sie ein Objekt als Wörterbuch verwenden möchten, ist es am besten, wenn es keinen Prototyp hat. Deshalb werden prototypenlose Objekte auch als dict-Objekte bezeichnet. In ES6 müssen Sie nicht einmal den Eigenschaftsschlüssel '__proto__' für dict-Objekte escapen, da er keine spezielle Funktionalität auslöst.

__proto__ als Operator in einem Objekt-Literal ermöglicht es Ihnen, dict-Objekte prägnanter zu erstellen

const dictObj = {
    __proto__: null,
    yes: true,
    no: false,
};

Beachten Sie, dass Sie in ES6 normalerweise die eingebaute Datenstruktur Map den dict-Objekten vorziehen sollten, insbesondere wenn die Schlüssel nicht fest sind.

14.6.3.4 __proto__ und JSON

Vor ES6 konnte in einer JavaScript-Engine Folgendes passieren

> JSON.parse('{"__proto__": []}') instanceof Array
true

Da __proto__ in ES6 ein Getter/Setter ist, funktioniert JSON.parse() einwandfrei, da es Eigenschaften definiert, nicht aber zuweist (wenn es ordnungsgemäß implementiert ist, eine ältere Version von V8 hat zugewiesen).

JSON.stringify() wird auch nicht von __proto__ beeinflusst, da es nur eigene Eigenschaften berücksichtigt. Objekte, die eine eigene Eigenschaft mit dem Namen __proto__ haben, funktionieren einwandfrei

> JSON.stringify({['__proto__']: true})
'{"__proto__":true}'

14.6.4 Erkennen der Unterstützung für ES6-style __proto__

Die Unterstützung für ES6-style __proto__ variiert von Engine zu Engine. Konsultieren Sie die Kompatibilitätstabelle für ECMAScript 6 von kangax für Informationen zum aktuellen Stand.

Die folgenden beiden Abschnitte beschreiben, wie Sie programmatisch erkennen können, ob eine Engine eine der beiden Arten von __proto__ unterstützt.

14.6.4.1 Feature: __proto__ als Getter/Setter

Ein einfacher Test für den Getter/Setter

var supported = {}.hasOwnProperty.call(Object.prototype, '__proto__');

Ein ausgefeilterer Test

var desc = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
var supported = (
    typeof desc.get === 'function' && typeof desc.set === 'function'
);
14.6.4.2 Feature: __proto__ als Operator in einem Objektliteral

Sie können den folgenden Test verwenden

var supported = Object.getPrototypeOf({__proto__: null}) === null;

14.6.5 __proto__ wird als „dunder proto“ ausgesprochen

Das Umklammern von Namen mit doppelten Unterstrichen ist eine gängige Praxis in Python, um Namenskonflikte zwischen Metadaten (wie __proto__) und Daten (benutzerdefinierte Eigenschaften) zu vermeiden. Diese Praxis wird in JavaScript nie verbreitet sein, da es jetzt Symbole für diesen Zweck gibt. Wir können uns jedoch die Python-Community als Quelle für Ideen ansehen, wie man doppelte Unterstriche ausspricht.

Die folgende Aussprache wurde von Ned Batchelder vorgeschlagen

Eine unangenehme Sache beim Programmieren in Python: Es gibt viele doppelte Unterstriche. Zum Beispiel haben die Standardmethodennamen unterhalb des syntaktischen Zuckers Namen wie __getattr__, Konstruktoren sind __init__, eingebaute Operatoren können mit __add__ überladen werden und so weiter. [...]

Mein Problem mit dem doppelten Unterstrich ist, dass er schwer auszusprechen ist. Wie spricht man __init__ aus? „Unterstrich Unterstrich Init Unterstrich Unterstrich“? „Dunder Init Dunder“? Nur „Init“ scheint etwas Wichtiges auszulassen.

Ich habe eine Lösung: Doppelunterstrich soll „dunder“ ausgesprochen werden. Also ist __init__ „dunder init dunder“ oder einfach nur „dunder init“.

Daher wird __proto__ als „dunder proto“ ausgesprochen. Die Chancen, dass sich diese Aussprache durchsetzt, sind gut, JavaScript-Erfinder Brendan Eich verwendet sie.

14.6.6 Empfehlungen für __proto__

Es ist schön, wie gut ES6 __proto__ von etwas Obscurem in etwas Einfaches und leicht Verständliches verwandelt.

Ich empfehle dennoch, es nicht zu verwenden. Es ist effektiv eine als veraltet markierte Funktion und nicht Teil des Kernstandards. Sie können sich nicht darauf verlassen, dass es für Code, der auf allen Engines laufen muss, vorhanden ist.

Weitere Empfehlungen

14.7 Aufzählbarkeit in ECMAScript 6

Aufzählbarkeit ist ein Attribut von Objekteigenschaften. Dieser Abschnitt erklärt, wie es in ECMAScript 6 funktioniert. Lassen Sie uns zuerst untersuchen, was Attribute sind.

14.7.1 Eigenschaftsattribute

Jedes Objekt hat null oder mehr Eigenschaften. Jede Eigenschaft hat einen Schlüssel und drei oder mehr Attribute, benannte Slots, die die Daten der Eigenschaft speichern (mit anderen Worten, eine Eigenschaft ist selbst sehr ähnlich einem JavaScript-Objekt oder einem Record mit Feldern in einer Datenbank).

ECMAScript 6 unterstützt die folgenden Attribute (ebenso wie ES5)

Sie können die Attribute einer Eigenschaft über Object.getOwnPropertyDescriptor() abrufen, welches die Attribute als JavaScript-Objekt zurückgibt.

> const obj = { foo: 123 };
> Object.getOwnPropertyDescriptor(obj, 'foo')
{ value: 123,
  writable: true,
  enumerable: true,
  configurable: true }

Dieser Abschnitt erklärt, wie das Attribut enumerable in ES6 funktioniert. Alle anderen Attribute und wie man Attribute ändert, wird in Abschn. „Eigenschaftsattribute und Eigenschaftsdeskriptoren“ in „Speaking JavaScript“ erklärt.

14.7.2 Konstrukte, die von der Aufzählbarkeit betroffen sind

ECMAScript 5

ECMAScript 6

for-in ist die einzige eingebaute Operation, bei der die Aufzählbarkeit für geerbte Eigenschaften wichtig ist. Alle anderen Operationen arbeiten nur mit eigenen Eigenschaften.

14.7.3 Anwendungsfälle für Aufzählbarkeit

Leider ist Aufzählbarkeit ein ziemlich eigenartiges Merkmal. Dieser Abschnitt präsentiert mehrere Anwendungsfälle dafür und argumentiert, dass ihre Nützlichkeit, abgesehen vom Schutz von Altsystemen vor dem Brechen, begrenzt ist.

14.7.3.1 Anwendungsfall: Verbergen von Eigenschaften vor der for-in-Schleife

Die for-in-Schleife durchläuft alle aufzählbaren Eigenschaften eines Objekts, eigene und geerbte. Daher wird das Attribut enumerable verwendet, um Eigenschaften zu verbergen, die nicht durchlaufen werden sollen. Dies war der Grund für die Einführung der Aufzählbarkeit in ECMAScript 1.

14.7.3.1.1 Nicht-Aufzählbarkeit in der Sprache

Nicht aufzählbare Eigenschaften treten an den folgenden Stellen in der Sprache auf

Der Hauptgrund, warum all diese Eigenschaften nicht aufzählbar sind, ist, sie (insbesondere die geerbten) vor Altsystemen zu verbergen, die die for-in-Schleife oder $.extend() (und ähnliche Operationen, die sowohl geerbte als auch eigene Eigenschaften kopieren; siehe nächster Abschnitt) verwenden. Beide Operationen sollten in ES6 vermieden werden. Das Verbergen stellt sicher, dass die Altsysteme nicht kaputt gehen.

14.7.3.2 Anwendungsfall: Markieren von Eigenschaften, die nicht kopiert werden sollen
14.7.3.2.1 Historische Präzedenzfälle

Wenn es um das Kopieren von Eigenschaften geht, gibt es zwei wichtige historische Präzedenzfälle, die die Aufzählbarkeit berücksichtigen

Probleme mit dieser Art des Kopierens von Eigenschaften

Die einzige Instanzeigenschaft, die in der Standardbibliothek nicht aufzählbar ist, ist die Eigenschaft length von Arrays. Diese Eigenschaft muss jedoch nur versteckt werden, da sie sich über andere Eigenschaften magisch selbst aktualisiert. Sie können keine solche magische Eigenschaft für Ihre eigenen Objekte erstellen (außer mit einem Proxy).

14.7.3.2.2 ES6: Object.assign()

In ES6 kann Object.assign(target, source_1, source_2, ···) verwendet werden, um die Quellen in das Ziel zu verschmelzen. Alle eigenen aufzählbaren Eigenschaften der Quellen werden berücksichtigt (d. h. die Schlüssel können sowohl Strings als auch Symbole sein). Object.assign() verwendet eine „get“-Operation, um einen Wert aus einer Quelle zu lesen, und eine „set“-Operation, um einen Wert in das Ziel zu schreiben.

In Bezug auf die Aufzählbarkeit setzt Object.assign() die Tradition von Object.extend() und $.extend() fort. Zitiert Yehuda Katz

Object.assign würde den ausgetretenen Pfad aller zirkulierenden extend()-APIs pflastern. Wir dachten, der Präzedenzfall, aufzählbare Methoden in diesen Fällen nicht zu kopieren, sei Grund genug, damit Object.assign dieses Verhalten hat.

Mit anderen Worten: Object.assign() wurde mit einem Upgrade-Pfad von $.extend() (und ähnlichem) im Hinterkopf erstellt. Sein Ansatz ist sauberer als der von $.extend, da er geerbte Eigenschaften ignoriert.

14.7.3.3 Eigenschaften als privat markieren

Wenn Sie eine Eigenschaft nicht aufzählbar machen, kann sie von Object.keys() und der for-in-Schleife nicht mehr gesehen werden. In Bezug auf diese Mechanismen ist die Eigenschaft privat.

Es gibt jedoch mehrere Probleme mit diesem Ansatz

14.7.3.4 Eigene Eigenschaften vor JSON.stringify() verbergen

JSON.stringify() schließt Eigenschaften, die nicht aufzählbar sind, nicht in seine Ausgabe ein. Sie können daher die Aufzählbarkeit verwenden, um zu bestimmen, welche eigenen Eigenschaften nach JSON exportiert werden sollen. Dieser Anwendungsfall ähnelt dem Markieren von Eigenschaften als privat, dem vorherigen Anwendungsfall. Er ist aber auch anders, da es hier mehr um den Export geht und leicht andere Überlegungen gelten. Zum Beispiel: Kann ein Objekt vollständig aus JSON rekonstruiert werden?

Eine Alternative zur Angabe, wie ein Objekt in JSON konvertiert werden soll, ist die Verwendung von toJSON()

const obj = {
    foo: 123,
    toJSON() {
        return { bar: 456 };
    },
};
JSON.stringify(obj); // '{"bar":456}'

Ich finde toJSON() für den aktuellen Anwendungsfall sauberer als Aufzählbarkeit. Es gibt Ihnen auch mehr Kontrolle, da Sie Eigenschaften exportieren können, die auf dem Objekt nicht existieren.

14.7.4 Namensinkonsistenzen

Im Allgemeinen bedeutet ein kürzerer Name, dass nur aufzählbare Eigenschaften berücksichtigt werden

Reflect.ownKeys() weicht jedoch von dieser Regel ab, es ignoriert die Aufzählbarkeit und gibt die Schlüssel aller Eigenschaften zurück. Zusätzlich wird ab ES6 die folgende Unterscheidung getroffen

Daher wäre ein besserer Name für Object.keys() jetzt Object.names().

14.7.5 Ausblick

Meiner Meinung nach eignet sich Aufzählbarkeit nur zum Verbergen von Eigenschaften vor der for-in-Schleife und $.extend() (und ähnlichen Operationen). Beides sind Legacy-Funktionen, die Sie in neuem Code vermeiden sollten. Was die anderen Anwendungsfälle betrifft

Ich bin mir nicht sicher, was die beste Strategie für die Aufzählbarkeit in Zukunft ist. Wenn wir ab ES6 so getan hätten, als ob sie nicht existieren würde (außer um Prototyp-Eigenschaften nicht aufzählbar zu machen, damit alte Codes nicht kaputt gehen), hätten wir die Aufzählbarkeit schließlich als veraltet kennzeichnen können. Jedoch berücksichtigt Object.assign() die Aufzählbarkeit, was dieser Strategie zuwiderläuft (aber aus einem gültigen Grund, der Abwärtskompatibilität).

In meinem eigenen ES6-Code verwende ich keine Aufzählbarkeit, außer (implizit) für Klassen, deren prototype-Methoden nicht aufzählbar sind.

Schließlich vermisse ich bei der Verwendung einer interaktiven Kommandozeile gelegentlich eine Operation, die alle Eigenschaftsschlüssel eines Objekts zurückgibt, nicht nur die eigenen (Reflect.ownKeys). Eine solche Operation würde eine schöne Übersicht über den Inhalt eines Objekts bieten.

14.8 Anpassen grundlegender Sprachoperationen über bekannte Symbole

Dieser Abschnitt erklärt, wie Sie grundlegende Sprachoperationen mithilfe der folgenden bekannten Symbole als Eigenschaftsschlüssel anpassen können

14.8.1 Eigenschaftsschlüssel Symbol.hasInstance (Methode)

Ein Objekt C kann das Verhalten des instanceof-Operators über eine Methode mit dem Schlüssel Symbol.hasInstance anpassen, die die folgende Signatur hat

[Symbol.hasInstance](potentialInstance : any)

x instanceof C funktioniert in ES6 wie folgt

14.8.1.1 Verwendung in der Standardbibliothek

Die einzige Methode in der Standardbibliothek, die diesen Schlüssel hat, ist

Dies ist die Implementierung von instanceof, die alle Funktionen (einschließlich Klassen) standardmäßig verwenden. Zitiert aus der Spezifikation

Diese Eigenschaft ist nicht beschreibbar und nicht konfigurierbar, um Manipulationen zu verhindern, die verwendet werden könnten, um die Ziel Funktion einer gebundenen Funktion global offenzulegen.

Die Manipulation ist möglich, da der traditionelle instanceof-Algorithmus, OrdinaryHasInstance(), instanceof auf die Ziel Funktion anwendet, wenn er auf eine gebundene Funktion trifft.

Da diese Eigenschaft schreibgeschützt ist, können Sie sie nicht durch Zuweisung überschreiben, wie bereits erwähnt.

14.8.1.2 Beispiel: Prüfen, ob ein Wert ein Objekt ist

Als Beispiel implementieren wir ein Objekt ReferenceType, dessen „Instanzen“ alle Objekte sind, nicht nur Objekte, die Instanzen von Object sind (und daher Object.prototype in ihren Prototypketten haben).

const ReferenceType = {
    [Symbol.hasInstance](value) {
        return (value !== null
            && (typeof value === 'object'
                || typeof value === 'function'));
    }
};
const obj1 = {};
console.log(obj1 instanceof Object); // true
console.log(obj1 instanceof ReferenceType); // true

const obj2 = Object.create(null);
console.log(obj2 instanceof Object); // false
console.log(obj2 instanceof ReferenceType); // true

14.8.2 Eigenschaftsschlüssel Symbol.toPrimitive (Methode)

Symbol.toPrimitive ermöglicht es einem Objekt, seine Konvertierung (automatische Umwandlung) in einen primitiven Wert anzupassen.

Viele JavaScript-Operationen wandeln Werte in die benötigten Typen um.

Die folgenden sind die gängigsten Typen, in die Werte umgewandelt werden

Daher ist für Zahlen und Strings der erste Schritt, sicherzustellen, dass ein Wert irgendeine Art von Primitiv ist. Dies wird von der internen Spezifikationsoperation ToPrimitive() gehandhabt, die drei Modi hat

Der Standardmodus wird nur verwendet von

Wenn der Wert ein Primitiv ist, ist ToPrimitive() bereits abgeschlossen. Andernfalls ist der Wert ein Objekt obj, das wie folgt in ein Primitiv umgewandelt wird

Dieser normale Algorithmus kann überschrieben werden, indem einem Objekt eine Methode mit der folgenden Signatur gegeben wird

[Symbol.toPrimitive](hint : 'default' | 'string' | 'number')

In der Standardbibliothek gibt es zwei solche Methoden

14.8.2.1 Beispiel

Der folgende Code demonstriert, wie sich die Konvertierung auf das Objekt obj auswirkt.

const obj = {
    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case 'number':
                return 123;
            case 'string':
                return 'str';
            case 'default':
                return 'default';
            default:
                throw new Error();
        }
    }
};
console.log(2 * obj); // 246
console.log(3 + obj); // '3default'
console.log(obj == 'default'); // true
console.log(String(obj)); // 'str'

14.8.3 Eigenschaftsschlüssel Symbol.toStringTag (String)

In ES5 und früher hatte jedes Objekt die interne eigene Eigenschaft [[Class]], deren Wert seinen Typ andeutete. Sie konnten ihn nicht direkt abrufen, aber sein Wert war Teil des Strings, der von Object.prototype.toString() zurückgegeben wurde, weshalb diese Methode als Alternative zu typeof für Typüberprüfungen verwendet wurde.

In ES6 gibt es keinen internen Slot [[Class]] mehr, und die Verwendung von Object.prototype.toString() für Typüberprüfungen wird nicht empfohlen. Um die Abwärtskompatibilität dieser Methode zu gewährleisten, wurde die öffentliche Eigenschaft mit dem Schlüssel Symbol.toStringTag eingeführt. Man könnte sagen, dass sie [[Class]] ersetzt.

Object.prototype.toString() funktioniert jetzt wie folgt

14.8.3.1 Standard-toString-Tags

Die Standardwerte für verschiedene Arten von Objekten sind in der folgenden Tabelle aufgeführt.

Wert toString-Tag
undefined 'Undefined'
null 'Null'
Ein Array-Objekt 'Array'
Ein String-Objekt 'String'
arguments 'Arguments'
Etwas Aufrufbares 'Function'
Ein Fehlerobjekt 'Error'
Ein Boolesches Objekt 'Boolean'
Ein Zahlenobjekt 'Number'
Ein Datumsobjekt 'Date'
Ein reguläres Ausdrucksobjekt 'RegExp'
(Sonstiges) 'Object'

Die meisten Prüfungen in der linken Spalte werden durch die Betrachtung interner Slots durchgeführt. Zum Beispiel, wenn ein Objekt den internen Slot [[Call]] hat, ist es aufrufbar.

Die folgende Interaktion demonstriert die Standard-toString-Tags.

> Object.prototype.toString.call(null)
'[object Null]'
> Object.prototype.toString.call([])
'[object Array]'
> Object.prototype.toString.call({})
'[object Object]'
> Object.prototype.toString.call(Object.create(null))
'[object Object]'
14.8.3.2 Überschreiben des Standard-toString-Tags

Wenn ein Objekt eine eigene oder geerbte Eigenschaft mit dem Schlüssel Symbol.toStringTag hat, überschreibt deren Wert den Standard-toString-Tag. Zum Beispiel

> ({}.toString())
'[object Object]'
> ({[Symbol.toStringTag]: 'Foo'}.toString())
'[object Foo]'

Instanzen von benutzerdefinierten Klassen erhalten den Standard-toString-Tag (von Objekten)

class Foo { }
console.log(new Foo().toString()); // [object Object]

Eine Option zum Überschreiben des Standards ist über einen Getter

class Bar {
    get [Symbol.toStringTag]() {
      return 'Bar';
    }
}
console.log(new Bar().toString()); // [object Bar]

In der JavaScript-Standardbibliothek gibt es die folgenden benutzerdefinierten toString-Tags. Objekte, die keine globalen Namen haben, werden mit Prozentzeichen zitiert (zum Beispiel: %TypedArray%).

Alle eingebauten Eigenschaften mit dem Schlüssel Symbol.toStringTag haben den folgenden Eigenschaftsdeskriptor

{
    writable: false,
    enumerable: false,
    configurable: true,
}

Wie bereits erwähnt, können Sie die Zuweisung nicht verwenden, um diese Eigenschaften zu überschreiben, da sie schreibgeschützt sind.

14.8.4 Eigenschaftsschlüssel Symbol.unscopables (Objekt)

Symbol.unscopables ermöglicht es einem Objekt, einige Eigenschaften vor der with-Anweisung zu verbergen.

Der Grund dafür ist, dass TC39 neue Methoden zu Array.prototype hinzufügen kann, ohne alten Code zu beeinträchtigen. Beachten Sie, dass aktueller Code with selten verwendet, was im strikten Modus verboten ist und daher ES6-Module (die implizit im strikten Modus stehen).

Warum das Hinzufügen von Methoden zu Array.prototype Code beeinträchtigen würde, der with verwendet (wie das weit verbreitete Ext JS 4.2.1)? Schauen Sie sich den folgenden Code an. Die Existenz einer Eigenschaft Array.prototype.values beeinträchtigt foo(), wenn Sie sie mit einem Array aufrufen

function foo(values) {
    with (values) {
        console.log(values.length); // abc (*)
    }
}
Array.prototype.values = { length: 'abc' };
foo([]);

Innerhalb der with-Anweisung werden alle Eigenschaften von values zu lokalen Variablen, die sogar values selbst überschatten. Wenn values also eine Eigenschaft values hat, protokolliert die Anweisung in Zeile * values.values.length und nicht values.length.

Symbol.unscopables wird in der Standardbibliothek nur einmal verwendet

14.9 FAQ: Objektliterale

14.9.1 Kann ich super in Objektliteralen verwenden?

Ja, das können Sie! Details werden im Kapitel über Klassen erklärt.

Weiter: 15. Klassen