JavaScript für ungeduldige Programmierer (ES2021-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

28 Einzelne Objekte



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)

  1. Einzelne Objekte (dieses Kapitel): Wie funktionieren Objekte, die grundlegenden Bausteine der objektorientierten Programmierung in JavaScript, isoliert?
  2. Prototypketten (nächstes Kapitel): Jedes Objekt hat eine Kette von null oder mehr Prototypobjekten. Prototypen sind der Kernmechanismus für Vererbung in JavaScript.
  3. Klassen (nächstes Kapitel): Klassen in JavaScript sind Fabriken für Objekte. Die Beziehung zwischen einer Klasse und ihren Instanzen basiert auf prototypischer Vererbung.
  4. Subklassenbildung (nächstes Kapitel): Die Beziehung zwischen einer Unterklasse und ihrer Oberklasse basiert ebenfalls auf prototypischer Vererbung.
Figure 8: This book introduces object-oriented programming in JavaScript in four steps.

28.1 Was ist ein Objekt?

In JavaScript

28.1.1 Rollen von Objekten: Record vs. Dictionary

Objekte spielen zwei Rollen in JavaScript

Diese Rollen beeinflussen, wie Objekte in diesem Kapitel erklärt werden

28.2 Objekte als Records

Lassen Sie uns zuerst die Rolle des Records von Objekten untersuchen.

28.2.1 Objekt-Literale: Eigenschaften

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

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' ]

28.2.2 Objekt-Literale: Kurzschreibweisen für Eigenschaftswerte

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 }
);

28.2.3 Eigenschaften abrufen

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);

28.2.4 Eigenschaften setzen

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']);

28.2.5 Objekt-Literale: Methoden

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.

28.2.6 Objekt-Literale: Accessoren

Es gibt zwei Arten von Accessoren in JavaScript

28.2.6.1 Getter

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');
28.2.6.2 Setter

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

28.3 Spreading in Objekt-Literale (...) [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).

28.3.1 Anwendungsfall für Spreading: Objekte kopieren

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 change

Allerdings 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.

28.3.2 Anwendungsfall für Spreading: Standardwerte für fehlende Eigenschaften

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'});

28.3.3 Anwendungsfall für Spreading: Eigenschaften nicht-destruktiv ändern

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

28.4 Methoden und die spezielle Variable this

28.4.1 Methoden sind Eigenschaften, deren Werte Funktionen sind

Kehren 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}”`;
  },
};

28.4.2 Die spezielle Variable this

Betrachten 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).

28.4.3 Methoden und .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

  1. Einer zum Zugriff auf Eigenschaften: obj.prop
  2. Ein anderer zum Aufrufen von Methoden: obj.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.

28.4.4 Methoden und .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.

28.4.5 this-Fallstrick: Methoden extrahieren

Wir 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”');
28.4.5.1 Beispiel: Eine Methode extrahieren

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));
28.4.5.2 Wie man den Fallstrick beim Extrahieren von Methoden vermeidet

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

28.4.6 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)
},
28.4.6.1 Vermeidung des Fallstricks des versehentlichen Shadowings von this

Wenn 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

28.4.7 Der Wert von 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

Wir können this auch in allen üblichen Top-Level-Geltungsbereichen aufrufen

  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.

28.5 Optional Chaining für Eigenschaftszugriffe und Methodenaufrufe [ES2020] (fortgeschritten)

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 call

Die grobe Idee ist

28.5.1 Beispiel: Optionaler statischer Eigenschaftszugriff

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]
);
28.5.1.1 Handhabung von Standardwerten über Nullish Coalescing

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)']
);

28.5.2 Die Operatoren im Detail (fortgeschritten)

28.5.2.1 Optionaler statischer Eigenschaftszugriff

Die folgenden beiden Ausdrücke sind äquivalent.

o?.prop
(o !== undefined && o !== null) ? o.prop : undefined

Beispiele

assert.equal(undefined?.prop, undefined);
assert.equal(null?.prop,      undefined);
assert.equal({prop:1}?.prop,  1);
28.5.2.2 Optionaler dynamischer Eigenschaftszugriff

Die folgenden beiden Ausdrücke sind äquivalent.

o?.[«expr»]
(o !== undefined && o !== null) ? o[«expr»] : undefined

Beispiele

const key = 'prop';
assert.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1);
28.5.2.3 Optionaler Funktions- oder Methodenaufruf

Die folgenden beiden Ausdrücke sind äquivalent.

f?.(arg0, arg1)
(f !== undefined && f !== null) ? f(arg0, arg1) : undefined

Beispiele

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.

28.5.3 Short-Circuiting (fortgeschritten)

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

28.5.4 Häufig gestellte Fragen

28.5.4.1 Warum gibt es Punkte in 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) : []
28.5.4.2 Warum ergibt 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.

28.6 Objekte als Dictionaries (fortgeschritten)

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).

28.6.1 Beliebige feste Strings als Eigenschaftsschlüssel

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!');

28.6.2 Berechnete Eigenschaftsschlüssel

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

28.6.3 Der 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);
28.6.3.1 Prüfen, ob eine Eigenschaft existiert, über Wahrheitswerte

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'

28.6.4 Eigenschaften löschen

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), []);

28.6.5 Eigenschaftsschlüssel auflisten

Tabelle 19: Standardbibliotheksmethoden zum Auflisten von eigenen (nicht vererbten) Eigenschaftsschlüsseln. Alle geben Arrays mit Strings und/oder Symbolen zurück.
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.

28.6.5.1 Enumerabilität

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.

28.6.6 Eigenschaftswerte über Object.values() auflisten

Object.values() listet die Werte aller enumerable Eigenschaften eines Objekts auf

const obj = {foo: 1, bar: 2};
assert.deepEqual(
  Object.values(obj),
  [1, 2]);

28.6.7 Eigenschaftseinträge über 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

28.6.8 Eigenschaften werden deterministisch aufgelistet

Eigene (nicht vererbte) Eigenschaften von Objekten werden immer in der folgenden Reihenfolge aufgelistet

  1. Eigenschaften mit String-Schlüsseln, die ganzzahlige Indizes enthalten (einschließlich Array-Indizes)
    In aufsteigender numerischer Reihenfolge
  2. Verbleibende Eigenschaften mit String-Schlüsseln
    In der Reihenfolge, in der sie hinzugefügt wurden
  3. Eigenschaften mit Symbol-Schlüsseln
    In der Reihenfolge, in der sie hinzugefügt wurden

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.

28.6.9 Objekte zusammensetzen über 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.

28.6.9.1 Beispiel: 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);
}
28.6.9.2 Beispiel: 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);
}
28.6.9.3 Eine einfache Implementierung von 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

28.6.10 Die Tücken der Verwendung eines Objekts als Wörterbuch

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), []);
28.6.10.1 Sichere Verwendung von Objekten als Wörterbücher

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

  Übung: Ein Objekt als Wörterbuch verwenden

exercises/single-objects/simple_dict_test.mjs

28.7 Standardmethoden (fortgeschritten)

Object.prototype definiert mehrere Standardmethoden, die überschrieben werden können, um zu konfigurieren, wie ein Objekt von der Sprache behandelt wird. Zwei wichtige sind

28.7.1 .toString()

.toString() bestimmt, wie Objekte in Zeichenketten konvertiert werden

> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'

28.7.2 .valueOf()

.valueOf() bestimmt, wie Objekte in Zahlen konvertiert werden

> Number({valueOf() { return 123 }})
123
> Number({})
NaN

28.8 Fortgeschrittene Themen

Die folgenden Unterabschnitte geben kurze Überblicke über einige fortgeschrittene Themen.

28.8.1 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.

28.8.2 Objekte einfrieren [ES5]

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.

28.8.3 Eigenschaftsattribute und Eigenschaftsdeskriptoren [ES5]

So wie Objekte aus Eigenschaften bestehen, bestehen Eigenschaften aus Attributen. Der Wert einer Eigenschaft ist nur eines von mehreren Attributen. Andere sind

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.