...) [ES2018]thisthis.call().bind()this-Fallstrick: Methoden extrahierenthis-Fallstrick: versehentliches Überschatten von thisthis in verschiedenen Kontexten (fortgeschritten)in-Operator: Gibt es eine Eigenschaft mit einem bestimmten Schlüssel?Object.keys() etc.Object.values()Object.entries() [ES2017]Object.fromEntries() [ES2019]Object.hasOwn(): Ist eine gegebene Eigenschaft eine eigene (nicht vererbte) Eigenschaft? [ES2022]In diesem Buch wird der objektorientierte Programmierstil (OOP) von JavaScript in vier Schritten eingeführt. Dieses Kapitel behandelt Schritt 1 und 2; das nächste Kapitel behandelt Schritt 3 und 4. Die Schritte sind (Abb. 8)
Ein Objekt über ein Objektliteral erstellen (beginnt und endet mit einer geschweiften Klammer)
const myObject = { // object literal
myProperty: 1,
myMethod() {
return 2;
}, // comma!
get myAccessor() {
return this.myProperty;
}, // comma!
set myAccessor(value) {
this.myProperty = value;
}, // last comma is optional
};
assert.equal(
myObject.myProperty, 1
);
assert.equal(
myObject.myMethod(), 2
);
assert.equal(
myObject.myAccessor, 1
);
myObject.myAccessor = 3;
assert.equal(
myObject.myProperty, 3
);Die Möglichkeit, Objekte direkt zu erstellen (ohne Klassen), ist eines der Highlights von JavaScript.
Einfügen in Objekte
const original = {
a: 1,
b: {
c: 3,
},
};
// Spreading (...) copies one object “into” another one:
const modifiedCopy = {
...original, // spreading
d: 4,
};
assert.deepEqual(
modifiedCopy,
{
a: 1,
b: {
c: 3,
},
d: 4,
}
);
// Caveat: spreading copies shallowly (property values are shared)
modifiedCopy.a = 5; // does not affect `original`
modifiedCopy.b.c = 6; // affects `original`
assert.deepEqual(
original,
{
a: 1, // unchanged
b: {
c: 6, // changed
},
},
);Wir können das Einfügen auch verwenden, um eine unveränderte (flache) Kopie eines Objekts zu erstellen
const exactCopy = {...obj};Prototypen sind der grundlegende Vererbungsmechanismus von JavaScript. Selbst Klassen basieren darauf. Jedes Objekt hat null oder ein Objekt als Prototyp. Das letztere Objekt kann ebenfalls einen Prototyp haben usw. Im Allgemeinen erhalten wir Ketten von Prototypen.
Prototypen werden wie folgt verwaltet
// `obj1` has no prototype (its prototype is `null`)
const obj1 = Object.create(null); // (A)
assert.equal(
Object.getPrototypeOf(obj1), null // (B)
);
// `obj2` has the prototype `proto`
const proto = {
protoProp: 'protoProp',
};
const obj2 = {
__proto__: proto, // (C)
objProp: 'objProp',
}
assert.equal(
Object.getPrototypeOf(obj2), proto
);Hinweise
Jedes Objekt erbt alle Eigenschaften seines Prototyps
// `obj2` inherits .protoProp from `proto`
assert.equal(
obj2.protoProp, 'protoProp'
);
assert.deepEqual(
Reflect.ownKeys(obj2),
['objProp'] // own properties of `obj2`
);Die nicht geerbten Eigenschaften eines Objekts werden als seine eigenen Eigenschaften bezeichnet.
Der wichtigste Anwendungsfall für Prototypen ist, dass mehrere Objekte Methoden gemeinsam nutzen können, indem sie sie von einem gemeinsamen Prototyp erben.
Objekte in JavaScript
Es gibt zwei Arten, Objekte in JavaScript zu verwenden
Objekte mit festem Layout: So verwendet, funktionieren Objekte wie Datensätze in Datenbanken. Sie haben eine feste Anzahl von Eigenschaften, deren Schlüssel zur Entwicklungszeit bekannt sind. Ihre Werte haben im Allgemeinen unterschiedliche Typen.
const fixedLayoutObject = {
product: 'carrot',
quantity: 4,
};Dictionary-Objekte: So verwendet, funktionieren Objekte wie Nachschlagetabellen oder Maps. Sie haben eine variable Anzahl von Eigenschaften, deren Schlüssel zur Entwicklungszeit nicht bekannt sind. Alle ihre Werte haben den gleichen Typ.
const dictionaryObject = {
['one']: 1,
['two']: 2,
};Beachten Sie, dass die beiden Arten auch gemischt werden können: Einige Objekte sind sowohl Objekte mit festem Layout als auch Dictionary-Objekte.
Die Arten der Objektverwendung beeinflussen, wie sie in diesem Kapitel erklärt werden
Lassen Sie uns zunächst Objekte mit festem Layout untersuchen.
Objektliterale sind eine Möglichkeit, Objekte mit festem Layout zu erstellen. Sie sind ein herausragendes Merkmal von JavaScript: Wir können Objekte direkt erstellen – keine Klassen erforderlich! Das ist ein Beispiel
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
};Im Beispiel haben wir ein Objekt über ein Objektliteral erstellt, das mit geschweiften Klammern {} beginnt und endet. Darin haben wir zwei Eigenschaften (Schlüssel-Wert-Einträge) definiert.
first und den Wert 'Jane'.last und den Wert 'Doe'.Seit ES5 sind nachgestellte Kommas in Objektliteralen erlaubt.
Wir werden später weitere Möglichkeiten zur Angabe von Eigenschaftsschlüsseln sehen, aber bei dieser Art der Angabe müssen sie den Regeln von JavaScript-Variablennamen folgen. Zum Beispiel können wir first_name als Eigenschaftsschlüssel verwenden, aber nicht first-name). Reservierte Wörter sind jedoch erlaubt.
const obj = {
if: true,
const: true,
};Um die Auswirkungen verschiedener Operationen auf Objekte zu überprüfen, verwenden wir in diesem Teil des Kapitels gelegentlich Object.keys(). Es listet Eigenschaftsschlüssel auf.
> Object.keys({a:1, b:2})
[ 'a', 'b' ]Immer wenn der Wert einer Eigenschaft über eine Variable definiert wird, die denselben Namen wie der Schlüssel hat, können wir den Schlüssel weglassen.
function createPoint(x, y) {
return {x, y}; // Same as: {x: x, y: y}
}
assert.deepEqual(
createPoint(9, 2),
{ x: 9, y: 2 }
);So rufen wir eine Eigenschaft ab (lesen sie) (Zeile A)
const jane = {
first: 'Jane',
last: 'Doe',
};
// Get property .first
assert.equal(jane.first, 'Jane'); // (A)Das Abrufen einer unbekannten Eigenschaft ergibt undefined
assert.equal(jane.unknownProperty, undefined);So setzen wir eine Eigenschaft (schreiben in sie hinein) (Zeile A)
const obj = {
prop: 1,
};
assert.equal(obj.prop, 1);
obj.prop = 2; // (A)
assert.equal(obj.prop, 2);Wir haben gerade eine vorhandene Eigenschaft durch Setzen geändert. Wenn wir eine unbekannte Eigenschaft setzen, erstellen wir einen neuen Eintrag.
const obj = {}; // empty object
assert.deepEqual(
Object.keys(obj), []);
obj.unknownProperty = 'abc';
assert.deepEqual(
Object.keys(obj), ['unknownProperty']);Der folgende Code zeigt, wie die Methode .says() über ein Objektliteral erstellt wird.
const jane = {
first: 'Jane', // value property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
}, // comma as separator (optional at end)
};
assert.equal(jane.says('hello'), 'Jane says “hello”');Während des Methodenaufrufs jane.says('hello') wird jane als Empfänger des Methodenaufrufs bezeichnet und der speziellen Variablen this zugewiesen (mehr zu this in §28.5 „Methoden und die spezielle Variable this“). Das ermöglicht der Methode .says() den Zugriff auf die Geschwister-Eigenschaft .first in Zeile A.
Ein Accessor wird über eine Syntax innerhalb eines Objektliterals definiert, die wie Methoden aussieht: ein Getter und/oder ein Setter (d.h. jeder Accessor hat einen oder beide davon).
Das Aufrufen eines Accessors sieht aus wie das Zugreifen auf einen Wert-Eigenschaft
Ein Getter wird erstellt, indem einer Methodendefinition der Modifikator get vorangestellt wird.
const jane = {
first: 'Jane',
last: 'Doe',
get full() {
return `${this.first} ${this.last}`;
},
};
assert.equal(jane.full, 'Jane Doe');
jane.first = 'John';
assert.equal(jane.full, 'John Doe');Ein Setter wird erstellt, indem einer Methodendefinition der Modifikator set vorangestellt wird.
const jane = {
first: 'Jane',
last: 'Doe',
set full(fullName) {
const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
},
};
jane.full = 'Richard Roe';
assert.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe'); Übung: Erstellen eines Objekts über ein Objektliteral
exercises/objects/color_point_object_test.mjs
...) [ES2018]Innerhalb eines Objektliterals fügt eine eingefügte Eigenschaft die Eigenschaften eines anderen Objekts zum aktuellen hinzu.
> const obj = {one: 1, two: 2};
> {...obj, three: 3}
{ one: 1, two: 2, three: 3 }const obj1 = {one: 1, two: 2};
const obj2 = {three: 3};
assert.deepEqual(
{...obj1, ...obj2, four: 4},
{one: 1, two: 2, three: 3, four: 4}
);Wenn Eigenschaftsschlüssel kollidieren, „gewinnt“ die zuletzt genannte Eigenschaft.
> const obj = {one: 1, two: 2, three: 3};
> {...obj, one: true}
{ one: true, two: 2, three: 3 }
> {one: true, ...obj}
{ one: 1, two: 2, three: 3 }Alle Werte sind einfügbar, auch undefined und null.
> {...undefined}
{}
> {...null}
{}
> {...123}
{}
> {...'abc'}
{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}
{ '0': 'a', '1': 'b' }Die Eigenschaft .length von Strings und Arrays wird von dieser Art von Operation versteckt (sie ist nicht aufzählbar; siehe §28.8.1 „Eigenschaftsattribute und Eigenschaftsdeskriptoren [ES5]“ für weitere Informationen).
Das Einfügen umfasst Eigenschaften, deren Schlüssel Symbole sind (die von Object.keys(), Object.values() und Object.entries() ignoriert werden).
const symbolKey = Symbol('symbolKey');
const obj = {
stringKey: 1,
[symbolKey]: 2,
};
assert.deepEqual(
{...obj, anotherStringKey: 3},
{
stringKey: 1,
[symbolKey]: 2,
anotherStringKey: 3,
}
);Wir können das Einfügen verwenden, um eine Kopie eines Objekts original zu erstellen.
const copy = {...original};Vorsicht – das Kopieren ist flach: copy ist ein neues Objekt mit Duplikaten aller Eigenschaften (Schlüssel-Wert-Einträge) von original. Aber wenn Eigenschaftswerte Objekte sind, dann werden diese selbst nicht kopiert; sie werden zwischen original und copy geteilt. Betrachten wir ein Beispiel.
const original = { a: 1, b: {prop: true} };
const copy = {...original};Die erste Ebene von copy ist tatsächlich eine Kopie: Wenn wir Eigenschaften auf dieser Ebene ändern, hat das keine Auswirkungen auf das Original.
copy.a = 2;
assert.deepEqual(
original, { a: 1, b: {prop: true} }); // no changeJedoch werden tiefere Ebenen nicht kopiert. Zum Beispiel wird der Wert von .b zwischen Original und Kopie geteilt. Das Ändern von .b in der Kopie ändert es auch im Original.
copy.b.prop = false;
assert.deepEqual(
original, { a: 1, b: {prop: false} }); JavaScript hat keine integrierte Unterstützung für Tiefenkopien
Tiefenkopien von Objekten (bei denen alle Ebenen kopiert werden) sind notorisch schwierig generisch zu erstellen. Daher hat JavaScript keine integrierte Operation dafür (vorerst). Wenn wir eine solche Operation benötigen, müssen wir sie selbst implementieren.
Wenn eine der Eingaben unseres Codes ein Objekt mit Daten ist, können wir Eigenschaften optional machen, indem wir Standardwerte angeben, die verwendet werden, wenn diese Eigenschaften fehlen. Eine Technik dafür ist ein Objekt, dessen Eigenschaften die Standardwerte enthalten. Im folgenden Beispiel ist dieses Objekt DEFAULTS.
const DEFAULTS = {alpha: 'a', beta: 'b'};
const providedData = {alpha: 1};
const allData = {...DEFAULTS, ...providedData};
assert.deepEqual(allData, {alpha: 1, beta: 'b'});Das Ergebnis, das Objekt allData, wird durch Kopieren von DEFAULTS und Überschreiben seiner Eigenschaften mit denen von providedData erstellt.
Aber wir brauchen kein Objekt, um die Standardwerte anzugeben; wir können sie auch einzeln innerhalb des Objektliterals angeben.
const providedData = {alpha: 1};
const allData = {alpha: 'a', beta: 'b', ...providedData};
assert.deepEqual(allData, {alpha: 1, beta: 'b'});Bisher haben wir eine Möglichkeit kennengelernt, eine Eigenschaft .alpha eines Objekts zu ändern: Wir setzen sie (Zeile A) und verändern das Objekt. Das heißt, diese Art der Änderung einer Eigenschaft ist destruktiv.
const obj = {alpha: 'a', beta: 'b'};
obj.alpha = 1; // (A)
assert.deepEqual(obj, {alpha: 1, beta: 'b'});Mit dem Einfügen können wir .alpha nicht-destruktiv ändern – wir erstellen eine Kopie von obj, bei der .alpha einen anderen Wert hat.
const obj = {alpha: 'a', beta: 'b'};
const updatedObj = {...obj, alpha: 1};
assert.deepEqual(updatedObj, {alpha: 1, beta: 'b'}); Übung: Eine Eigenschaft nicht-destruktiv über Einfügen aktualisieren (fester Schlüssel)
exercises/objects/update_name_test.mjs
Object.assign() [ES6]Object.assign() ist eine Hilfsmethode.
Object.assign(target, source_1, source_2, ···)Dieser Ausdruck weist alle Eigenschaften von source_1 target zu, dann alle Eigenschaften von source_2 usw. Am Ende gibt er target zurück – zum Beispiel:
const target = { a: 1 };
const result = Object.assign(
target,
{b: 2},
{c: 3, b: true});
assert.deepEqual(
result, { a: 1, b: true, c: 3 });
// target was modified and returned:
assert.equal(result, target);Die Anwendungsfälle für Object.assign() ähneln denen für eingefügte Eigenschaften. In gewisser Weise fügt es destruktiv ein.
thisKehren wir zum Beispiel zurück, das zur Einführung von Methoden verwendet wurde.
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};Überraschenderweise sind Methoden Funktionen.
assert.equal(typeof jane.says, 'function');Warum ist das so? Wir haben im Kapitel über aufrufbare Werte gelernt, dass normale Funktionen mehrere Rollen spielen. Methode ist eine dieser Rollen. Daher sieht jane intern ungefähr so aus.
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
},
};thisBetrachten Sie den folgenden Code
const obj = {
someMethod(x, y) {
assert.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
}
};
obj.someMethod('a', 'b'); // (B)In Zeile B ist obj der Empfänger eines Methodenaufrufs. Er wird der Funktion, die in obj.someMethod gespeichert ist, über einen impliziten (versteckten) Parameter namens this übergeben (Zeile A).
Wie man
this versteht
Der beste Weg, this zu verstehen, ist als impliziter Parameter von normalen Funktionen (und daher auch von Methoden).
.call()Methoden sind Funktionen und Funktionen haben selbst Methoden. Eine dieser Methoden ist .call(). Betrachten wir ein Beispiel, um zu verstehen, wie diese Methode funktioniert.
Im vorherigen Abschnitt gab es diese Methodenaufrufung:
obj.someMethod('a', 'b')Diese Aufrufung ist äquivalent zu
obj.someMethod.call(obj, 'a', 'b');Was wiederum äquivalent ist zu
const func = obj.someMethod;
func.call(obj, 'a', 'b');.call() macht den normalerweise impliziten Parameter this explizit: Wenn eine Funktion über .call() aufgerufen wird, ist der erste Parameter this, gefolgt von den regulären (expliziten) Funktionsparametern.
Nebenbei bemerkt bedeutet dies, dass es tatsächlich zwei verschiedene Punktoperatoren gibt:
obj.propobj.prop()Sie unterscheiden sich dadurch, dass (2) nicht nur (1) gefolgt vom Funktionsaufrufoperator () ist. Stattdessen stellt (2) zusätzlich einen Wert für this bereit.
.bind().bind() ist eine weitere Methode von Funktions-Objekten. Im folgenden Code verwenden wir .bind(), um die Methode .says() in die eigenständige Funktion func() umzuwandeln.
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
},
};
const func = jane.says.bind(jane, 'hello');
assert.equal(func(), 'Jane says “hello”');Das Setzen von this auf jane über .bind() ist hier entscheidend. Andernfalls würde func() nicht richtig funktionieren, da this in Zeile A verwendet wird. Im nächsten Abschnitt werden wir untersuchen, warum das so ist.
this-Fallstrick: Methoden extrahierenWir wissen jetzt einiges über Funktionen und Methoden und sind bereit, uns mit dem größten Fallstrick im Zusammenhang mit Methoden und this zu befassen: das Aufrufen einer aus einem Objekt extrahierten Methode als Funktion kann fehlschlagen, wenn wir nicht vorsichtig sind.
Im folgenden Beispiel scheitern wir, wenn wir die Methode jane.says() extrahieren, sie in der Variablen func speichern und func als Funktion aufrufen.
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
const func = jane.says; // extract the method
assert.throws(
() => func('hello'), // (A)
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
});In Zeile A machen wir einen normalen Funktionsaufruf. Und bei normalen Funktionsaufrufen ist this undefined (wenn der Striktmodus aktiv ist, was er fast immer ist). Zeile A ist daher äquivalent zu
assert.throws(
() => jane.says.call(undefined, 'hello'), // `this` is undefined!
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
}
);Wie beheben wir das? Wir müssen .bind() verwenden, um die Methode .says() zu extrahieren.
const func2 = jane.says.bind(jane);
assert.equal(func2('hello'), 'Jane says “hello”');Das .bind() stellt sicher, dass this immer jane ist, wenn wir func() aufrufen.
Wir können auch Pfeilfunktionen verwenden, um Methoden zu extrahieren.
const func3 = text => jane.says(text);
assert.equal(func3('hello'), 'Jane says “hello”');Das Folgende ist eine vereinfachte Version von Code, den wir in der tatsächlichen Webentwicklung sehen könnten.
class ClickHandler {
constructor(id, elem) {
this.id = id;
elem.addEventListener('click', this.handleClick); // (A)
}
handleClick(event) {
alert('Clicked ' + this.id);
}
}In Zeile A extrahieren wir die Methode .handleClick() nicht richtig. Stattdessen sollten wir Folgendes tun:
const listener = this.handleClick.bind(this);
elem.addEventListener('click', listener);
// Later, possibly:
elem.removeEventListener('click', listener);Jeder Aufruf von .bind() erstellt eine neue Funktion. Deshalb müssen wir das Ergebnis irgendwo speichern, wenn wir es später entfernen wollen.
Leider gibt es keinen einfachen Ausweg aus dem Fallstrick des Extrahierens von Methoden: Wann immer wir eine Methode extrahieren, müssen wir vorsichtig sein und es richtig machen – zum Beispiel, indem wir this binden oder eine Pfeilfunktion verwenden.
Übung: Eine Methode extrahieren
exercises/objects/method_extraction_exrc.mjs
this-Fallstrick: versehentliches Überschatten von this Das versehentliche Überschatten von
this ist nur bei normalen Funktionen ein Problem
Pfeilfunktionen überschatten this nicht.
Betrachten Sie das folgende Problem: Wenn wir uns innerhalb einer normalen Funktion befinden, können wir nicht auf das this des umgebenden Geltungsbereichs zugreifen, da die normale Funktion ihr eigenes this hat. Anders ausgedrückt, eine Variable in einem inneren Geltungsbereich verdeckt eine Variable in einem äußeren Geltungsbereich. Das nennt man Überschatten. Der folgende Code ist ein Beispiel.
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
});
},
};
assert.throws(
() => prefixer.prefixStringArray(['a', 'b']),
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'prefix')",
}
);In Zeile A möchten wir auf das this von .prefixStringArray() zugreifen. Aber das können wir nicht, da die umgebende normale Funktion ihr eigenes this hat, das das this der Methode überschattet (und den Zugriff darauf blockiert). Der Wert des ersteren this ist undefined, da der Callback als Funktion aufgerufen wird. Das erklärt die Fehlermeldung.
Der einfachste Weg, dieses Problem zu beheben, ist eine Pfeilfunktion, die kein eigenes this hat und daher nichts überschattet.
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
(x) => {
return this.prefix + x;
});
},
};
assert.deepEqual(
prefixer.prefixStringArray(['a', 'b']),
['==> a', '==> b']);Wir können this auch in eine andere Variable speichern (Zeile A), damit es nicht überschattet wird.
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
});
},Eine weitere Option ist, das feste this für den Callback über .bind() anzugeben (Zeile A).
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
}.bind(this)); // (A)
},Zuletzt erlaubt .map() uns, einen Wert für this anzugeben (Zeile A), den es beim Aufrufen des Callbacks verwendet.
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
},
this); // (A)
},thisWenn wir den Ratschlag in §25.3.4 „Empfehlung: Spezialisierte Funktionen gegenüber normalen Funktionen bevorzugen“ befolgen, können wir den Fallstrick des versehentlichen Überschattens von this vermeiden. Hier ist eine Zusammenfassung:
Verwenden Sie Pfeilfunktionen als anonyme Inline-Funktionen. Sie haben this nicht als impliziten Parameter und überschatten es nicht.
Für benannte eigenständige Funktionsdeklarationen können wir entweder Pfeilfunktionen oder Funktionsdeklarationen verwenden. Wenn wir letzteres tun, müssen wir sicherstellen, dass this in ihren Körpern nicht erwähnt wird.
this in verschiedenen Kontexten (fortgeschritten)Was ist der Wert von this in verschiedenen Kontexten?
Innerhalb einer aufrufbaren Entität hängt der Wert von this davon ab, wie die aufrufbare Entität aufgerufen wird und welche Art von aufrufbarer Entität es ist.
this === undefined (im Striktmodus)this ist dasselbe wie im umgebenden Geltungsbereich (lexikalisches this)this ist der Empfänger des Aufrufs.new: this bezieht sich auf die neu erstellte Instanz.Wir können auch auf this in allen gängigen Top-Level-Geltungsbereichen zugreifen.
<script>-Element: this === globalThisthis === undefinedthis === module.exports Tipp: So tun, als ob
this in Top-Level-Geltungsbereichen nicht existiert
Ich mache das gerne, weil Top-Level-this verwirrend ist und es bessere Alternativen für seine (wenigen) Anwendungsfälle gibt.
Die folgenden Arten von Optional Chaining-Operationen existieren.
obj?.prop // optional fixed property getting
obj?.[«expr»] // optional dynamic property getting
func?.(«arg0», «arg1») // optional function or method callDie grobe Idee ist:
undefined noch null ist, führe die Operation nach dem Fragezeichen aus.undefined zurück.Jede der drei Syntaxen wird später detaillierter behandelt. Hier sind einige erste Beispiele.
> null?.prop
undefined
> {prop: 1}?.prop
1
> null?.(123)
undefined
> String?.(123)
'123'Betrachten Sie die folgenden Daten.
const persons = [
{
surname: 'Zoe',
address: {
street: {
name: 'Sesame Street',
number: '123',
},
},
},
{
surname: 'Mariner',
},
{
surname: 'Carmen',
address: {
},
},
];Wir können Optional Chaining verwenden, um sicher Straßennamen zu extrahieren.
const streetNames = persons.map(
p => p.address?.street?.name);
assert.deepEqual(
streetNames, ['Sesame Street', undefined, undefined]
);Der Nullish Coalescing-Operator ermöglicht es uns, den Standardwert '(kein Name)' anstelle von undefined zu verwenden.
const streetNames = persons.map(
p => p.address?.street?.name ?? '(no name)');
assert.deepEqual(
streetNames, ['Sesame Street', '(no name)', '(no name)']
);Die folgenden beiden Ausdrücke sind äquivalent.
o?.prop
(o !== undefined && o !== null) ? o.prop : undefinedBeispiele
assert.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1);Die folgenden beiden Ausdrücke sind äquivalent.
o?.[«expr»]
(o !== undefined && o !== null) ? o[«expr»] : undefinedBeispiele
const key = 'prop';
assert.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1);Die folgenden beiden Ausdrücke sind äquivalent.
f?.(arg0, arg1)
(f !== undefined && f !== null) ? f(arg0, arg1) : undefinedBeispiele
assert.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123');Beachten Sie, dass dieser Operator einen Fehler verursacht, wenn seine linke Seite nicht aufrufbar ist.
assert.throws(
() => true?.(123),
TypeError);Warum? Die Idee ist, dass der Operator nur bewusste Auslassungen toleriert. Ein nicht aufrufbarer Wert (außer undefined und null) ist wahrscheinlich ein Fehler und sollte gemeldet und nicht umgangen werden.
In einer Kette von Eigenschaftsabrufen und Methodenaufrufen stoppt die Auswertung, sobald der erste optionale Operator auf undefined oder null auf seiner linken Seite trifft.
function invokeM(value) {
return value?.a.b.m(); // (A)
}
const obj = {
a: {
b: {
m() { return 'result' }
}
}
};
assert.equal(
invokeM(obj), 'result'
);
assert.equal(
invokeM(undefined), undefined // (B)
);Betrachten Sie invokeM(undefined) in Zeile B: undefined?.a ist undefined. Daher würden wir erwarten, dass .b in Zeile A fehlschlägt. Aber das tut es nicht: Der ?.-Operator trifft auf den Wert undefined und die Auswertung des gesamten Ausdrucks gibt sofort undefined zurück.
Dieses Verhalten unterscheidet sich von einem normalen Operator, bei dem JavaScript immer alle Operanden auswertet, bevor es den Operator auswertet. Es wird als Short-Circuiting bezeichnet. Andere Short-Circuiting-Operatoren sind:
(a && b): b wird nur ausgewertet, wenn a wahrheitsgemäß ist.(a || b): b wird nur ausgewertet, wenn a falsch ist.(c ? t : e): Wenn c wahrheitsgemäß ist, wird t ausgewertet. Andernfalls wird e ausgewertet.Optional Chaining hat auch Nachteile.
Eine Alternative zu Optional Chaining ist, die Informationen einmal an einem einzigen Ort zu extrahieren.
Mit einem der beiden Ansätze ist es möglich, Prüfungen durchzuführen und frühzeitig abzubrechen, wenn es Probleme gibt.
Weiterführende Lektüre
?.)?Sind Sie sich manchmal unsicher, ob der Optional Chaining-Operator mit einem Punkt (.?) oder einem Fragezeichen (?.) beginnt? Dann hilft Ihnen diese Eselsbrücke.
?) die linke Seite nicht nullish ist.) greifen Sie auf eine Eigenschaft zu.o?.[x] und f?.()?Die Syntax der folgenden beiden optionalen Operatoren ist nicht ideal.
obj?.[«expr»] // better: obj?[«expr»]
func?.(«arg0», «arg1») // better: func?(«arg0», «arg1»)Leider ist die weniger elegante Syntax notwendig, da die Unterscheidung der idealen Syntax (erster Ausdruck) vom Bedingungsoperator (zweiter Ausdruck) zu kompliziert ist.
obj?['a', 'b', 'c'].map(x => x+x)
obj ? ['a', 'b', 'c'].map(x => x+x) : []null?.prop den Wert undefined und nicht null?Der Operator ?. bezieht sich hauptsächlich auf seine rechte Seite: Existiert die Eigenschaft .prop? Wenn nicht, brechen Sie frühzeitig ab. Daher ist es selten nützlich, Informationen über seine linke Seite zu speichern. Nur ein einziger Wert für „frühe Terminierung“ vereinfacht jedoch die Dinge.
Objekte funktionieren am besten als Objekte mit festem Layout. Aber vor ES6 gab es keine Datenstruktur für Dictionaries in JavaScript (ES6 brachte Maps). Daher mussten Objekte als Dictionaries verwendet werden, was eine erhebliche Einschränkung auferlegte: Dictionary-Schlüssel mussten Strings sein (Symbole wurden ebenfalls mit ES6 eingeführt).
Wir betrachten zunächst Funktionen von Objekten, die sich auf Dictionaries beziehen, aber auch für Objekte mit festem Layout nützlich sind. Dieser Abschnitt endet mit Tipps zur tatsächlichen Verwendung von Objekten als Dictionaries. (Spoiler: Wenn möglich, ist es besser, Maps zu verwenden.)
Bisher haben wir immer Objekte mit festem Layout verwendet. Eigenschaftsschlüssel waren feste Token, die gültige Bezeichner sein mussten und intern zu Strings wurden.
const obj = {
mustBeAnIdentifier: 123,
};
// Get property
assert.equal(obj.mustBeAnIdentifier, 123);
// Set property
obj.mustBeAnIdentifier = 'abc';
assert.equal(obj.mustBeAnIdentifier, 'abc');Als nächstes werden wir über diese Einschränkung für Eigenschaftsschlüssel hinausgehen: In diesem Unterabschnitt verwenden wir beliebige feste Strings als Schlüssel. Im nächsten Unterabschnitt werden wir Schlüssel dynamisch berechnen.
Zwei Syntaxen ermöglichen es uns, beliebige Strings als Eigenschaftsschlüssel zu verwenden.
Erstens können wir beim Erstellen von Eigenschaftsschlüsseln über Objektliterale Eigenschaftsschlüssel zitieren (mit einfachen oder doppelten Anführungszeichen).
const obj = {
'Can be any string!': 123,
};Zweitens können wir beim Abrufen oder Setzen von Eigenschaften eckige Klammern mit darin enthaltenen Strings verwenden.
// Get property
assert.equal(obj['Can be any string!'], 123);
// Set property
obj['Can be any string!'] = 'abc';
assert.equal(obj['Can be any string!'], 'abc');Wir können diese Syntaxen auch für Methoden verwenden.
const obj = {
'A nice method'() {
return 'Yes!';
},
};
assert.equal(obj['A nice method'](), 'Yes!');Im vorherigen Unterabschnitt wurden Eigenschaftsschlüssel über feste Strings in Objektliteralen angegeben. In diesem Abschnitt lernen wir, wie man Eigenschaftsschlüssel dynamisch berechnet. Das ermöglicht uns die Verwendung von beliebigen Strings oder Symbolen.
Die Syntax für dynamisch berechnete Eigenschaftsschlüssel in Objektliteralen ist von der dynamischen Eigenschaftsabfrage inspiriert. Das heißt, wir können eckige Klammern verwenden, um Ausdrücke einzuschließen.
const obj = {
['Hello world!']: true,
['p'+'r'+'o'+'p']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
};
assert.equal(obj['Hello world!'], true);
assert.equal(obj.prop, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye');Der Hauptanwendungsfall für berechnete Schlüssel sind Symbole als Eigenschaftsschlüssel (Zeile A).
Beachten Sie, dass der eckige Klammer-Operator zum Abrufen und Setzen von Eigenschaften mit beliebigen Ausdrücken funktioniert.
assert.equal(obj['p'+'r'+'o'+'p'], 123);
assert.equal(obj['==> prop'.slice(4)], 123);Methoden können auch berechnete Eigenschaftsschlüssel haben.
const methodKey = Symbol();
const obj = {
[methodKey]() {
return 'Yes!';
},
};
assert.equal(obj[methodKey](), 'Yes!');Für den Rest dieses Kapitels werden wir hauptsächlich wieder feste Eigenschaftsschlüssel verwenden (da sie syntaktisch bequemer sind). Aber alle Funktionen sind auch für beliebige Strings und Symbole verfügbar.
Übung: Eine Eigenschaft nicht-destruktiv über Einfügen aktualisieren (berechneter Schlüssel)
exercises/objects/update_property_test.mjs
in-Operator: Gibt es eine Eigenschaft mit einem bestimmten Schlüssel?Der in-Operator prüft, ob ein Objekt eine Eigenschaft mit einem bestimmten Schlüssel hat.
const obj = {
alpha: 'abc',
beta: false,
};
assert.equal('alpha' in obj, true);
assert.equal('beta' in obj, true);
assert.equal('unknownKey' in obj, false);Wir können auch eine Wahrheitsprüfung verwenden, um festzustellen, ob eine Eigenschaft existiert.
assert.equal(
obj.alpha ? 'exists' : 'does not exist',
'exists');
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');Die vorherigen Prüfungen funktionieren, weil obj.alpha wahrheitsgemäß ist und weil das Lesen einer fehlenden Eigenschaft undefined zurückgibt (was falsch ist).
Es gibt jedoch eine wichtige Einschränkung: Wahrheitsprüfungen schlagen fehl, wenn die Eigenschaft existiert, aber einen falschen Wert hat (undefined, null, false, 0, "", etc.).
assert.equal(
obj.beta ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'Wir können Eigenschaften über den delete Operator löschen
const obj = {
myProp: 123,
};
assert.deepEqual(Object.keys(obj), ['myProp']);
delete obj.myProp;
assert.deepEqual(Object.keys(obj), []);Aufzählbarkeit ist ein Attribut einer Eigenschaft. Nicht aufzählbare Eigenschaften werden von einigen Operationen ignoriert – z. B. von Object.keys() und beim Verstreuen von Eigenschaften. Standardmäßig sind die meisten Eigenschaften aufzählbar. Das nächste Beispiel zeigt, wie das geändert wird und wie es sich auf das Verstreuen auswirkt.
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4,
enumerable: false,
},
});
// Non-enumerable properties are ignored by spreading:
assert.deepEqual(
{...obj},
{
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
);Object.defineProperties() wird später in diesem Kapitel erklärt. Die nächste Unterüberschrift zeigt, wie diese Operationen von der Aufzählbarkeit beeinflusst werden
Object.keys() etc.| aufzählbar | nicht aufzählbar | string | symbol | |
|---|---|---|---|---|
Object.keys() |
✔ |
✔ |
||
Object.getOwnPropertyNames() |
✔ |
✔ |
✔ |
|
Object.getOwnPropertySymbols() |
✔ |
✔ |
✔ |
|
Reflect.ownKeys() |
✔ |
✔ |
✔ |
✔ |
Jede der Methoden in Tab. 19 gibt ein Array mit den eigenen Eigenschaftsschlüsseln des Parameters zurück. In den Namen der Methoden sehen wir, dass folgende Unterscheidung getroffen wird
Um die vier Operationen zu demonstrieren, greifen wir das Beispiel aus der vorherigen Unterüberschrift wieder auf
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
Object.keys(obj),
['enumerableStringKey']
);
assert.deepEqual(
Object.getOwnPropertyNames(obj),
['enumerableStringKey', 'nonEnumStringKey']
);
assert.deepEqual(
Object.getOwnPropertySymbols(obj),
[enumerableSymbolKey, nonEnumSymbolKey]
);
assert.deepEqual(
Reflect.ownKeys(obj),
[
'enumerableStringKey', 'nonEnumStringKey',
enumerableSymbolKey, nonEnumSymbolKey,
]
);Object.values()Object.values() listet die Werte aller aufzählbaren zeichenkettenbasierten Eigenschaften eines Objekts auf
const firstName = Symbol('firstName');
const obj = {
[firstName]: 'Jane',
lastName: 'Doe',
};
assert.deepEqual(
Object.values(obj),
['Doe']);Object.entries() [ES2017]Object.entries() listet alle aufzählbaren zeichenkettenbasierten Eigenschaften als Schlüssel-Wert-Paare auf. Jedes Paar wird als zweielementiges Array kodiert
const firstName = Symbol('firstName');
const obj = {
[firstName]: 'Jane',
lastName: 'Doe',
};
assert.deepEqual(
Object.entries(obj),
[
['lastName', 'Doe'],
]);Object.entries()Die folgende Funktion ist eine vereinfachte Version von Object.entries()
function entries(obj) {
return Object.keys(obj)
.map(key => [key, obj[key]]);
} Übung:
Object.entries()
exercises/objects/find_key_test.mjs
Eigene (nicht vererbte) Eigenschaften von Objekten werden immer in folgender Reihenfolge aufgelistet
Das folgende Beispiel zeigt, wie Eigenschaftsschlüssel nach diesen Regeln sortiert werden
> Object.keys({b:0,a:0, 10:0,2:0})
[ '2', '10', 'b', 'a' ] Die Reihenfolge von Eigenschaften
Die ECMAScript-Spezifikation beschreibt detaillierter, wie Eigenschaften geordnet werden.
Object.fromEntries() [ES2019]Gegeben ein Iterable von [Schlüssel, Wert]-Paaren, erstellt Object.fromEntries() ein Objekt
const symbolKey = Symbol('symbolKey');
assert.deepEqual(
Object.fromEntries(
[
['stringKey', 1],
[symbolKey, 2],
]
),
{
stringKey: 1,
[symbolKey]: 2,
}
);Object.fromEntries() macht das Gegenteil von Object.entries(). Während Object.entries() jedoch Symbol-basierte Eigenschaften ignoriert, tut Object.fromEntries() das nicht (siehe vorheriges Beispiel).
Um beide zu demonstrieren, werden wir sie verwenden, um zwei Hilfsfunktionen aus der Bibliothek Underscore in den nächsten Unter-Unterabschnitten zu implementieren.
pick()Die Underscore-Funktion pick() hat die folgende Signatur
pick(object, ...keys)Sie gibt eine Kopie von object zurück, die nur die Eigenschaften enthält, deren Schlüssel in den nachfolgenden Argumenten erwähnt sind
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
};
assert.deepEqual(
pick(address, 'street', 'number'),
{
street: 'Evergreen Terrace',
number: '742',
}
);Wir können pick() wie folgt implementieren
function pick(object, ...keys) {
const filteredEntries = Object.entries(object)
.filter(([key, _value]) => keys.includes(key));
return Object.fromEntries(filteredEntries);
}invert()Die Underscore-Funktion invert() hat die folgende Signatur
invert(object)Sie gibt eine Kopie von object zurück, in der die Schlüssel und Werte aller Eigenschaften vertauscht sind
assert.deepEqual(
invert({a: 1, b: 2, c: 3}),
{1: 'a', 2: 'b', 3: 'c'}
);Wir können invert() so implementieren
function invert(object) {
const reversedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(reversedEntries);
}Object.fromEntries()Die folgende Funktion ist eine vereinfachte Version von Object.fromEntries()
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key === 'symbol') {
coercedKey = key;
} else {
coercedKey = String(key);
}
result[coercedKey] = value;
}
return result;
} Übung: Verwendung von
Object.entries() und Object.fromEntries()
exercises/objects/omit_properties_test.mjs
Wenn wir einfache Objekte (erstellt über Objekt-Literale) als Wörterbücher verwenden, müssen wir auf zwei Tücken achten.
Die erste Tücke ist, dass der in-Operator auch geerbte Eigenschaften findet
const dict = {};
assert.equal('toString' in dict, true);Wir möchten, dass dict als leer behandelt wird, aber der in-Operator erkennt die Eigenschaften, die es von seinem Prototyp, Object.prototype, erbt.
Die zweite Tücke ist, dass wir den Eigenschaftsschlüssel __proto__ nicht verwenden können, da er spezielle Funktionen hat (er setzt den Prototyp des Objekts)
const dict = {};
dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);Wie vermeiden wir also die beiden Tücken?
Der folgende Code demonstriert die Verwendung von Prototypen-losen Objekten als Wörterbücher
const dict = Object.create(null); // prototype is `null`
assert.equal('toString' in dict, false); // (A)
dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);Wir haben beide Tücken vermieden
__proto__ über Object.prototype implementiert. Das bedeutet, dass es ausgeschaltet wird, wenn Object.prototype nicht in der Prototypenkette ist. Übung: Verwendung eines Objekts als Wörterbuch
exercises/objects/simple_dict_test.mjs
So wie Objekte aus Eigenschaften bestehen, bestehen Eigenschaften aus Attributen. Der Wert einer Eigenschaft ist nur eines von mehreren Attributen. Andere sind
writable: Ist es möglich, den Wert der Eigenschaft zu ändern?enumerable: Wird die Eigenschaft von Object.keys(), dem Verstreuen usw. berücksichtigt?Wenn wir eine der Operationen zur Handhabung von Eigenschaftsattributen verwenden, werden Attribute über Deskriptoren von Eigenschaften spezifiziert: Objekte, bei denen jede Eigenschaft ein Attribut darstellt. So lesen wir zum Beispiel die Attribute einer Eigenschaft obj.myProp
const obj = { myProp: 123 };
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'myProp'),
{
value: 123,
writable: true,
enumerable: true,
configurable: true,
});Und so ändern wir die Attribute von obj.myProp
assert.deepEqual(Object.keys(obj), ['myProp']);
// Hide property `myProp` from Object.keys()
// by making it non-enumerable
Object.defineProperty(obj, 'myProp', {
enumerable: false,
});
assert.deepEqual(Object.keys(obj), []);Weiterführende Lektüre
Object.freeze(obj) macht obj vollständig unveränderlich: Wir können Eigenschaften nicht ändern, keine Eigenschaften hinzufügen oder seinen Prototyp ändern – zum Beispiel
const frozen = Object.freeze({ x: 2, y: 5 });
assert.throws(
() => { frozen.x = 7 },
{
name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
});Unter der Haube ändert Object.freeze() die Attribute von Eigenschaften (z. B. macht es sie nicht beschreibbar) und Objekte (z. B. macht es sie nicht erweiterbar, was bedeutet, dass keine Eigenschaften mehr hinzugefügt werden können).
Es gibt eine Einschränkung: Object.freeze(obj) friert oberflächlich ein. Das heißt, nur die Eigenschaften von obj sind eingefroren, aber nicht Objekte, die in Eigenschaften gespeichert sind.
Weitere Informationen
Weitere Informationen zum Einfrieren und anderen Möglichkeiten, Objekte zu sperren, finden Sie in Deep JavaScript.
Prototypen sind der einzige Vererbungsmechanismus von JavaScript: Jedes Objekt hat einen Prototyp, der entweder null oder ein Objekt ist. Im letzteren Fall erbt das Objekt alle Eigenschaften des Prototyps.
In einem Objekt-Literal können wir den Prototyp über die spezielle Eigenschaft __proto__ setzen
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);Da ein Prototyp-Objekt selbst einen Prototyp haben kann, erhalten wir eine Kette von Objekten – die sogenannte Prototypenkette. Vererbung gibt uns den Eindruck, dass wir es mit einzelnen Objekten zu tun haben, aber wir haben es tatsächlich mit Ketten von Objekten zu tun.
Abb. 9 zeigt, wie die Prototypenkette von obj aussieht.
Nicht vererbte Eigenschaften werden eigene Eigenschaften genannt. obj hat eine eigene Eigenschaft, .objProp.
Einige Operationen berücksichtigen alle Eigenschaften (eigene und vererbte) – zum Beispiel das Abrufen von Eigenschaften
> const obj = { one: 1 };
> typeof obj.one // own
'number'
> typeof obj.toString // inherited
'function'Andere Operationen berücksichtigen nur eigene Eigenschaften – zum Beispiel Object.keys()
> Object.keys(obj)
[ 'one' ]Lesen Sie weiter für eine weitere Operation, die ebenfalls nur eigene Eigenschaften berücksichtigt: das Setzen von Eigenschaften.
Gegeben ein Objekt obj mit einer Kette von Prototyp-Objekten, ist es sinnvoll, dass das Setzen einer eigenen Eigenschaft von obj nur obj ändert. Aber das Setzen einer vererbten Eigenschaft über obj ändert ebenfalls nur obj. Es erstellt eine neue eigene Eigenschaft in obj, die die vererbte Eigenschaft überschreibt. Untersuchen wir, wie das mit dem folgenden Objekt funktioniert
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};Im nächsten Code-Snippet setzen wir die vererbte Eigenschaft obj.protoProp (Zeile A). Das „ändert“ sie durch Erstellen einer eigenen Eigenschaft: Beim Lesen von obj.protoProp wird zuerst die eigene Eigenschaft gefunden und ihr Wert *überschreibt* den Wert der vererbten Eigenschaft.
// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);
obj.protoProp = 'x'; // (A)
// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');
// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');Die Prototypenkette von obj ist in Abb. 10 dargestellt.
Empfehlungen für __proto__
Verwenden Sie __proto__ nicht als Pseudo-Eigenschaft (einen Setter für alle Instanzen von Object)
Object sind).Weitere Informationen zu diesem Feature finden Sie in §29.8.7 „Object.prototype.__proto__ (Accessor)“.
Die Verwendung von __proto__ in Objekt-Literalen zum Setzen von Prototypen ist anders: Es ist ein Feature von Objekt-Literalen, das keine Tücken hat.
Die empfohlenen Wege zum Abrufen und Setzen von Prototypen sind
Abrufen des Prototyps eines Objekts
Object.getPrototypeOf(obj: Object) : ObjectDie beste Zeit, den Prototyp eines Objekts zu setzen, ist, wenn wir es erstellen. Wir können dies über __proto__ in einem Objekt-Literal oder über
Object.create(proto: Object) : ObjectWenn wir es tun müssen, können wir Object.setPrototypeOf() verwenden, um den Prototyp eines vorhandenen Objekts zu ändern. Das kann sich jedoch negativ auf die Leistung auswirken.
So werden diese Features verwendet
const proto1 = {};
const proto2a = {};
const proto2b = {};
const obj1 = {
__proto__: proto1,
a: 1,
b: 2,
};
assert.equal(Object.getPrototypeOf(obj1), proto1);
const obj2 = Object.create(
proto2a,
{
a: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
},
}
);
assert.equal(Object.getPrototypeOf(obj2), proto2a);
Object.setPrototypeOf(obj2, proto2b);
assert.equal(Object.getPrototypeOf(obj2), proto2b);Bisher bedeutete „proto ist ein Prototyp von obj“ immer „proto ist ein *direkter* Prototyp von obj“. Aber es kann auch lockerer verwendet werden und bedeuten, dass proto in der Prototypenkette von obj liegt. Diese lockerere Beziehung kann über .isPrototypeOf() geprüft werden
Zum Beispiel
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(c.isPrototypeOf(a), false);
assert.equal(a.isPrototypeOf(a), false);Weitere Informationen zu dieser Methode finden Sie in §29.8.5 „Object.prototype.isPrototypeOf()“.
Object.hasOwn(): Ist eine gegebene Eigenschaft eigen (nicht geerbt)? [ES2022]Der in-Operator (Zeile A) prüft, ob ein Objekt eine gegebene Eigenschaft hat. Im Gegensatz dazu prüft Object.hasOwn() (Zeilen B und C), ob eine Eigenschaft eigen ist.
const proto = {
protoProp: 'protoProp',
};
const obj = {
__proto__: proto,
objProp: 'objProp',
}
assert.equal('protoProp' in obj, true); // (A)
assert.equal(Object.hasOwn(obj, 'protoProp'), false); // (B)
assert.equal(Object.hasOwn(proto, 'protoProp'), true); // (C) Alternative vor ES2022:
.hasOwnProperty()
Vor ES2022 können wir ein anderes Feature verwenden: §29.8.8 „Object.prototype.hasOwnProperty()“. Dieses Feature hat Tücken, aber der referenzierte Abschnitt erklärt, wie man mit ihnen umgeht.
Betrachten Sie den folgenden Code
const jane = {
firstName: 'Jane',
describe() {
return 'Person named '+this.firstName;
},
};
const tarzan = {
firstName: 'Tarzan',
describe() {
return 'Person named '+this.firstName;
},
};
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');Wir haben zwei Objekte, die sehr ähnlich sind. Beide haben zwei Eigenschaften, deren Namen .firstName und .describe sind. Zusätzlich ist die Methode .describe() dieselbe. Wie können wir diese Methode vermeiden, zu duplizieren?
Wir können sie in ein Objekt PersonProto verschieben und dieses Objekt zum Prototyp von sowohl jane als auch tarzan machen
const PersonProto = {
describe() {
return 'Person named ' + this.firstName;
},
};
const jane = {
__proto__: PersonProto,
firstName: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
firstName: 'Tarzan',
};Der Name des Prototyps spiegelt wider, dass sowohl jane als auch tarzan Personen sind.
Abb. 11 illustriert, wie die drei Objekte verbunden sind: Die Objekte unten enthalten nun die Eigenschaften, die für jane und tarzan spezifisch sind. Das Objekt oben enthält die Eigenschaften, die zwischen ihnen geteilt werden.
Wenn wir den Methodenaufruf jane.describe() ausführen, verweist this auf den Empfänger dieses Methodenaufrufs, jane (in der unteren linken Ecke des Diagramms). Deshalb funktioniert die Methode immer noch. tarzan.describe() funktioniert ähnlich.
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');Ein Blick auf das nächste Kapitel über Klassen – so sind Klassen intern organisiert
§29.3 „Die Interna von Klassen“ erklärt dies genauer.
Prinzipiell sind Objekte ungeordnet. Der Hauptgrund für die Ordnung von Eigenschaften ist, dass Operationen, die Einträge, Schlüssel oder Werte auflisten, deterministisch sind. Das hilft z. B. beim Testen.
Quiz
Siehe Quiz-App.