...) [ES2018]thisthis.call().bind()this-Fallstrick: Methoden extrahierenthis-Fallstrick: versehentliches Shadowing von thisthis in verschiedenen Kontexten (fortgeschritten)in-Operator: Gibt es eine Eigenschaft mit einem bestimmten Schlüssel?Object.values() auflistenObject.entries() auflisten [ES2017]Object.fromEntries() [ES2019].toString().valueOf()In diesem Buch wird der objektorientierte Programmierstil (OOP) von JavaScript in vier Schritten vorgestellt. Dieses Kapitel behandelt Schritt 1; das nächste Kapitel behandelt die Schritte 2-4. Die Schritte sind (Abb. 8)
In JavaScript
Objekte spielen zwei Rollen in JavaScript
Records: Objekte als Records haben eine feste Anzahl von Eigenschaften, deren Schlüssel zur Entwicklungszeit bekannt sind. Ihre Werte können unterschiedliche Typen haben.
Dictionaries: Objekte als Dictionaries haben eine variable Anzahl von Eigenschaften, deren Schlüssel zur Entwicklungszeit nicht bekannt sind. Alle ihre Werte haben denselben Typ.
Diese Rollen beeinflussen, wie Objekte in diesem Kapitel erklärt werden
Lassen Sie uns zuerst die Rolle des Records von Objekten untersuchen.
Objekt-Literale sind eine Möglichkeit, Objekte als Records zu erstellen. Sie sind ein herausragendes Merkmal von JavaScript: Wir können direkt Objekte erstellen – keine Klassen nötig! Dies ist ein Beispiel
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
};Im Beispiel haben wir ein Objekt über ein Objekt-Literal erstellt, das mit geschweiften Klammern {} beginnt und endet. Darin haben wir zwei Eigenschaften (Schlüssel-Wert-Paare) definiert
first und den Wert 'Jane'.last und den Wert 'Doe'.Seit ES5 sind nachgestellte Kommas in Objekt-Literalen erlaubt.
Wir werden später andere Wege sehen, Eigenschaftsschlüssel anzugeben, aber mit dieser Art der Angabe müssen sie den Regeln für 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, werden wir in diesem Teil des Kapitels gelegentlich Object.keys() verwenden. Es listet Eigenschaftsschlüssel auf
> Object.keys({a:1, b:2})
[ 'a', 'b' ]Immer wenn der Wert einer Eigenschaft über einen Variablennamen definiert wird und dieser Name mit dem Schlüssel übereinstimmt, können wir den Schlüssel weglassen.
function createPoint(x, y) {
return {x, y};
}
assert.deepEqual(
createPoint(9, 2),
{ x: 9, y: 2 }
);So rufen wir ab (lesen) eine Eigenschaft (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 (schreiben in) eine Eigenschaft
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 Objekt-Literal erstellt wird
const jane = {
first: 'Jane', // data 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 über this in §28.4 „Methoden und die spezielle Variable this“). Das ermöglicht der Methode .says() den Zugriff auf die Geschwister-Eigenschaft .first in Zeile A.
Es gibt zwei Arten von Accessoren in JavaScript
Ein Getter wird erstellt, indem einer Methodendefinition das 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 Objekt-Literal
exercises/single-objects/color_point_object_test.mjs
...) [ES2018]Innerhalb eines Funktionsaufrufs wandelt Spreading (...) die iterierten Werte eines iterierbaren Objekts in Argumente um.
Innerhalb eines Objekt-Literals fügt eine Spread-Eigenschaft die Eigenschaften eines anderen Objekts zum aktuellen hinzu
> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}
{ foo: 1, bar: 2, baz: 3 }Wenn Eigenschaftsschlüssel kollidieren, "gewinnt" die zuletzt genannte Eigenschaft
> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}
{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}
{ foo: 1, bar: 2, baz: 3 }Alle Werte sind spreadable, sogar 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 ausgeblendet (sie ist nicht enumerable; siehe §28.8.3 „Eigenschaftenattribute und Eigenschaftsdeskriptoren [ES5]“ für weitere Informationen).
Wir können Spreading 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-Paare) von original. Aber wenn Eigenschaftswerte Objekte sind, werden diese selbst nicht kopiert; sie werden zwischen original und copy geteilt. Sehen wir uns ein Beispiel an
const original = { a: 1, b: {foo: true} };
const copy = {...original};Die erste Ebene von copy ist tatsächlich eine Kopie: Wenn wir Eigenschaften auf dieser Ebene ändern, hat dies keine Auswirkungen auf das Original
copy.a = 2;
assert.deepEqual(
original, { a: 1, b: {foo: true} }); // no changeAllerdings 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.foo = false;
assert.deepEqual(
original, { a: 1, b: {foo: false} }); JavaScript hat keine eingebaute Unterstützung für Deep Copying
Deep Copies von Objekten (bei denen alle Ebenen kopiert werden) sind notorisch schwierig generisch zu erstellen. Daher hat JavaScript keine eingebaute 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 = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};
const allData = {...DEFAULTS, ...providedData};
assert.deepEqual(allData, {foo: 1, bar: 'b'});Das Ergebnis, das Objekt allData, wird erstellt, indem DEFAULTS kopiert und seine Eigenschaften mit denen von providedData überschrieben werden.
Aber wir brauchen kein Objekt, um die Standardwerte anzugeben; wir können sie auch einzeln innerhalb des Objekt-Literals angeben
const providedData = {foo: 1};
const allData = {foo: 'a', bar: 'b', ...providedData};
assert.deepEqual(allData, {foo: 1, bar: 'b'});Bisher haben wir eine Möglichkeit kennengelernt, eine Eigenschaft .foo eines Objekts zu ändern: Wir setzen sie (Zeile A) und mutieren das Objekt. Das heißt, diese Art der Änderung einer Eigenschaft ist destruktiv.
const obj = {foo: 'a', bar: 'b'};
obj.foo = 1; // (A)
assert.deepEqual(obj, {foo: 1, bar: 'b'});Mit Spreading können wir .foo nicht-destruktiv ändern – wir erstellen eine Kopie von obj, bei der .foo einen anderen Wert hat
const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
assert.deepEqual(updatedObj, {foo: 1, bar: 'b'}); Übung: Nicht-destruktives Aktualisieren einer Eigenschaft über Spreading (fester Schlüssel)
exercises/single-objects/update_name_test.mjs
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}”`;
},
};Etwas überraschend sind Methoden Funktionen
assert.equal(typeof jane.says, 'function');Warum ist das so? Wir haben im Kapitel über aufrufbare Werte gelernt, dass gewöhnliche Funktionen mehrere Rollen spielen. Methode ist eine dieser Rollen. Daher sieht jane im Grunde wie folgt 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 über einen impliziten (versteckten) Parameter mit dem Namen this an die Funktion übergeben, die in obj.someMethod gespeichert ist (Zeile A).
Das ist ein wichtiger Punkt: Der beste Weg, this zu verstehen, ist als impliziter Parameter von gewöhnlichen Funktionen (und damit auch von Methoden).
.call()Methoden sind Funktionen und in §25.7 „Methoden von Funktionen: .call(), .apply(), .bind()“ haben wir gesehen, dass Funktionen selbst Methoden haben. 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 ebenfalls äquivalent ist zu
const func = obj.someMethod;
func.call(obj, 'a', 'b');.call() macht den normalerweise impliziten Parameter this explizit: Beim Aufrufen einer Funktion über .call() 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 darin, 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() zu verwandeln
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 den größten Fallstrick im Zusammenhang mit Methoden und this anzusehen: das Aufrufen einer aus einem Objekt extrahierten Methode als normale Funktion kann fehlschlagen, wenn wir nicht vorsichtig sind.
Im folgenden Beispiel scheitern wir, wenn wir die Methode jane.says() extrahieren, in der Variable func speichern und func() als normale 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 property 'first' of undefined",
});In Zeile A machen wir einen normalen Funktionsaufruf. Und bei normalen Funktionsaufrufen ist this undefined (wenn Strict Mode 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 property 'first' of undefined",
});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 es so machen
elem.addEventListener('click', this.handleClick.bind(this));Leider gibt es keinen einfachen Ausweg aus dem Fallstrick beim Extrahieren 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/single-objects/method_extraction_exrc.mjs
this-Fallstrick: versehentliches Shadowing von this Versehentliches Shadowing von
this ist nur bei gewöhnlichen Funktionen ein Problem
Pfeilfunktionen schatten this nicht.
Betrachten Sie das folgende Problem: Wenn wir uns innerhalb einer gewöhnlichen Funktion befinden, können wir nicht auf das this des umgebenden Geltungsbereichs zugreifen, da die gewöhnliche Funktion ihr eigenes this hat. Mit anderen Worten, eine Variable in einem inneren Geltungsbereich versteckt eine Variable in einem äußeren Geltungsbereich. Das nennt man Shadowing. 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']),
/^TypeError: Cannot read property 'prefix' of undefined$/);In Zeile A wollen wir auf das this von .prefixStringArray() zugreifen. Aber das können wir nicht, da die umgebende gewöhnliche Funktion ihr eigenes this hat, das das this der Methode schattet (den Zugriff darauf blockiert). Der Wert des ersteren this ist undefined, da der Callback als normale Funktion aufgerufen wird. Das erklärt die Fehlermeldung.
Die einfachste Lösung für dieses Problem ist eine Pfeilfunktion, die kein eigenes this hat und daher nichts schattet
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 einer anderen Variable speichern (Zeile A), damit es nicht geschattet wird
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
});
},Eine weitere Option ist, einen festen this für den Callback über .bind() anzugeben (Zeile A)
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
}.bind(this)); // (A)
},Schließlich lässt uns .map() einen Wert für this angeben (Zeile A), den es beim Aufrufen des Callbacks verwendet
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
},
this); // (A)
},thisWenn Sie den Rat in §25.3.4 „Empfehlung: Spezialisierte Funktionen gegenüber gewöhnlichen Funktionen bevorzugen“ befolgen, können Sie den Fallstrick des versehentlichen Shadowings von this vermeiden. Dies ist eine Zusammenfassung
Verwenden Sie Pfeilfunktionen als anonyme Inline-Funktionen. Sie haben this nicht als impliziten Parameter und schatten es nicht.
Für benannte eigenständige Funktionsdeklarationen können Sie entweder Pfeilfunktionen oder Funktionsdeklarationen verwenden. Wenn Sie Letzteres tun, stellen Sie sicher, 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 Strict Mode)this ist dasselbe wie im umgebenden Geltungsbereich (lexikalisches this)this ist der Empfänger des Aufrufsnew: this bezieht sich auf die neu erstellte InstanzWir können this auch in allen üblichen Top-Level-Geltungsbereichen aufrufen
<script>-Element: this === globalThisthis === undefinedthis === module.exports Tipp: Tun Sie so, als ob
this in Top-Level-Geltungsbereichen nicht existiert
Ich mache das gerne, weil Top-Level this verwirrend und selten nützlich ist.
Es existieren die folgenden Arten von Optional Chaining-Operationen
obj?.prop // optional static property access
obj?.[«expr»] // optional dynamic property access
func?.(«arg0», «arg1») // optional function or method callDie grobe Idee ist
undefined noch null ist, dann führe die Operation nach dem Fragezeichen aus.undefined zurück.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 Straßennamen sicher 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 '(keine Straße)' 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 erzeugt, wenn seine linke Seite nicht aufrufbar ist
assert.throws(
() => true?.(123),
TypeError);Warum? Die Idee ist, dass der Operator nur absichtliche 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 Eigenschaftszugriffen und Funktions-/Methodenaufrufen stoppt die Auswertung, sobald der erste optionale Operator auf undefined oder null auf seiner linken Seite trifft
function isInvoked(obj) {
let invoked = false;
obj?.a.b.m(invoked = true);
return invoked;
}
assert.equal(
isInvoked({a: {b: {m() {}}}}), true);
// The left-hand side of ?. is undefined
// and the assignment is not executed
assert.equal(
isInvoked(undefined), false);Dieses Verhalten unterscheidet sich von einem normalen Operator/Funktion, bei dem JavaScript immer alle Operanden/Argumente auswertet, bevor der Operator/die Funktion ausgewertet wird. Es wird Short-Circuiting genannt. Andere Short-Circuiting-Operatoren
a && ba || bc ? t : eo?.[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 undefined und nicht null?Der Operator ?. bezieht sich hauptsächlich auf seine rechte Seite: Existiert die Eigenschaft .prop? Wenn nicht, brich früh ab. Daher ist es selten nützlich, Informationen über seine linke Seite zu speichern. Nur einen einzigen "frühen Abbruch"-Wert zu haben, vereinfacht jedoch die Dinge.
Objekte funktionieren am besten als Records. Aber vor ES6 hatte JavaScript keine Datenstruktur für Dictionaries (ES6 brachte Maps). Daher mussten Objekte als Dictionaries verwendet werden, was eine erhebliche Einschränkung mit sich brachte: 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 als Records nützlich sind. Dieser Abschnitt endet mit Tipps zur tatsächlichen Verwendung von Objekten als Dictionaries (Spoiler: Verwenden Sie Maps, wenn Sie können).
Bisher haben wir Objekte immer als Records verwendet. Eigenschaftsschlüssel waren feste Tokens, 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ächsten Schritt gehen wir über diese Einschränkung für Eigenschaftsschlüssel hinaus: In diesem Abschnitt verwenden wir beliebige feste Strings als Schlüssel. Im nächsten Unterabschnitt werden wir Schlüssel dynamisch berechnen.
Zwei Techniken erlauben uns, beliebige Strings als Eigenschaftsschlüssel zu verwenden.
Erstens, wenn wir Eigenschaftsschlüssel über Objekt-Literale erstellen, können wir Eigenschaftsschlüssel quotieren (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 Strings darin 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 Techniken auch für Methoden verwenden
const obj = {
'A nice method'() {
return 'Yes!';
},
};
assert.equal(obj['A nice method'](), 'Yes!');Bisher waren Eigenschaftsschlüssel in Objekt-Literalen immer feste Strings. In diesem Abschnitt lernen wir, wie man Eigenschaftsschlüssel dynamisch berechnet. Das ermöglicht uns, entweder beliebige Strings oder Symbole zu verwenden.
Die Syntax von dynamisch berechneten Eigenschaftsschlüsseln in Objekt-Literalen ist von der dynamischen Zugriffsweise auf Eigenschaften inspiriert. Das heißt, wir können eckige Klammern verwenden, um Ausdrücke zu umschließen
const obj = {
['Hello world!']: true,
['f'+'o'+'o']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
};
assert.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye');Der Hauptanwendungsfall für berechnete Schlüssel ist die Verwendung von Symbolen als Eigenschaftsschlüssel (Zeile A).
Beachten Sie, dass der eckige Klammeroperator zum Abrufen und Setzen von Eigenschaften mit beliebigen Ausdrücken funktioniert
assert.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 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 wieder hauptsächlich feste Eigenschaftsschlüssel verwenden (weil sie syntaktisch bequemer sind). Aber alle Funktionen sind auch für beliebige Strings und Symbole verfügbar.
Übung: Nicht-destruktives Aktualisieren einer Eigenschaft über Spreading (berechneter Schlüssel)
exercises/single-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 = {
foo: 'abc',
bar: false,
};
assert.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false);Wir können auch eine Wahrheitswertprüfung verwenden, um festzustellen, ob eine Eigenschaft existiert
assert.equal(
obj.foo ? 'exists' : 'does not exist',
'exists');
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');Die vorherigen Prüfungen funktionieren, weil obj.foo wahrheitsgemäß ist und weil das Lesen einer fehlenden Eigenschaft undefined zurückgibt (was falsch ist).
Es gibt jedoch eine wichtige Einschränkung: Wahrheitswertprüfungen schlagen fehl, wenn die Eigenschaft existiert, aber einen falsch positiven Wert hat (undefined, null, false, 0, "" usw.)
assert.equal(
obj.bar ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'Wir können Eigenschaften mit dem delete-Operator löschen
const obj = {
foo: 123,
};
assert.deepEqual(Object.keys(obj), ['foo']);
delete obj.foo;
assert.deepEqual(Object.keys(obj), []);| aufzählbar | nicht-e. | 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 können wir die folgende Unterscheidung erkennen
Der nächste Abschnitt beschreibt den Begriff enumerable und demonstriert jede der Methoden.
Enumerabilität ist ein Attribut einer Eigenschaft. Nicht-enumerable Eigenschaften werden von einigen Operationen ignoriert – zum Beispiel von Object.keys() (siehe Tab. 19) und von Spread-Eigenschaften. Standardmäßig sind die meisten Eigenschaften enumerable. Das nächste Beispiel zeigt, wie man das ändert. Es demonstriert auch die verschiedenen Möglichkeiten, Eigenschaftsschlüssel aufzulisten.
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,
},
});
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.defineProperties() wird später in diesem Kapitel erklärt.
Object.values() auflistenObject.values() listet die Werte aller enumerable Eigenschaften eines Objekts auf
const obj = {foo: 1, bar: 2};
assert.deepEqual(
Object.values(obj),
[1, 2]);Object.entries() auflisten [ES2017]Object.entries() listet Schlüssel-Wert-Paare von enumerable Eigenschaften auf. Jedes Paar wird als zweielementiges Array kodiert
const obj = {foo: 1, bar: 2};
assert.deepEqual(
Object.entries(obj),
[
['foo', 1],
['bar', 2],
]); Übung:
Object.entries()
exercises/single-objects/find_key_test.mjs
Eigene (nicht vererbte) Eigenschaften von Objekten werden immer in der folgenden Reihenfolge aufgelistet
Das folgende Beispiel demonstriert, wie Eigenschaftsschlüssel gemäß 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
assert.deepEqual(
Object.fromEntries([['foo',1], ['bar',2]]),
{
foo: 1,
bar: 2,
}
);Object.fromEntries() macht das Gegenteil von Object.entries().
Um beide zu demonstrieren, werden wir sie verwenden, um zwei Hilfsfunktionen aus der Bibliothek Underscore in den nächsten Unterunterabschnitten zu implementieren.
pick(object, ...keys)pick gibt eine Kopie von object zurück, die nur die Eigenschaften enthält, deren Schlüssel als Argumente erwähnt werden
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(object)invert gibt eine Kopie von object zurück, bei 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() wie folgt 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:
Object.entries() und Object.fromEntries()
exercises/single-objects/omit_properties_test.mjs
Wenn wir normale Objekte (erstellt über Objektliterale) als Wörterbücher verwenden, müssen wir zwei Tücken beachten.
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 Befugnisse hat (er setzt den Prototyp des Objekts)
const dict = {};
dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);Wie vermeidet man also die beiden Tücken?
Der folgende Code demonstriert die Verwendung von Objekten ohne Prototyp als Wörterbücher
const dict = Object.create(null); // no prototype
assert.equal('toString' in dict, false); // (A)
dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);Wir haben beide Tücken vermieden
__proto__ in modernem JavaScript über Object.prototype implementiert. Das bedeutet, dass es ausgeschaltet ist, wenn Object.prototype nicht in der Prototypenkette enthalten ist. Übung: Ein Objekt als Wörterbuch verwenden
exercises/single-objects/simple_dict_test.mjs
Object.prototype definiert mehrere Standardmethoden, die überschrieben werden können, um zu konfigurieren, wie ein Objekt von der Sprache behandelt wird. Zwei wichtige sind
.toString().valueOf().toString().toString() bestimmt, wie Objekte in Zeichenketten konvertiert werden
> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'.valueOf().valueOf() bestimmt, wie Objekte in Zahlen konvertiert werden
> Number({valueOf() { return 123 }})
123
> Number({})
NaNDie folgenden Unterabschnitte geben kurze Überblicke über einige fortgeschrittene Themen.
Object.assign() [ES6]Object.assign() ist eine Werkzeugmethode
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 = { foo: 1 };
const result = Object.assign(
target,
{bar: 2},
{baz: 3, bar: 4});
assert.deepEqual(
result, { foo: 1, bar: 4, baz: 3 });
// target was modified and returned:
assert.equal(result, target);Die Anwendungsfälle für Object.assign() ähneln denen für Spread-Eigenschaften. Auf eine Weise verteilt es destruktiv.
Object.freeze(obj) macht obj vollständig unveränderlich: Wir können Eigenschaften nicht ändern, keine neuen 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'/,
});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 über das Einfrieren und andere Möglichkeiten, Objekte zu schützen, finden Sie in Deep JavaScript.
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(), Spread usw. berücksichtigt?Wenn wir eine der Operationen zur Handhabung von Eigenschaftsattributen verwenden, werden Attribute über Eigenschaftsdeskriptoren angegeben: Objekte, bei denen jede Eigenschaft ein Attribut darstellt. Zum Beispiel werden so die Attribute einer Eigenschaft obj.foo ausgelesen
const obj = { foo: 123 };
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'foo'),
{
value: 123,
writable: true,
enumerable: true,
configurable: true,
});Und so werden die Attribute einer Eigenschaft obj.bar gesetzt
const obj = {
foo: 1,
bar: 2,
};
assert.deepEqual(Object.keys(obj), ['foo', 'bar']);
// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
enumerable: false,
});
assert.deepEqual(Object.keys(obj), ['foo']);Weiterführende Lektüre
Quiz
Siehe Quiz-App.