Deep JavaScript
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

11 Properties: Zuweisung vs. Definition



Es gibt zwei Arten, eine Eigenschaft prop eines Objekts obj zu erstellen oder zu ändern

Dieses Kapitel erklärt, wie sie funktionieren.

  Erforderliche Kenntnisse: Eigenschaftsattribute und Eigenschaftsdeskriptoren

Für dieses Kapitel sollten Sie mit Eigenschaftsattributen und Eigenschaftsdeskriptoren vertraut sein. Wenn nicht, lesen Sie §9 „Eigenschaftsattribute: Eine Einführung“.

11.1 Zuweisung vs. Definition

11.1.1 Zuweisung

Wir verwenden den Zuweisungsoperator =, um einer Eigenschaft .prop eines Objekts obj einen Wert value zuzuweisen

obj.prop = value

Dieser Operator funktioniert je nachdem, wie .prop aussieht, unterschiedlich

Das Hauptziel der Zuweisung ist also, Änderungen vorzunehmen. Deshalb unterstützt sie Setter.

11.1.2 Definition

Um eine Eigenschaft mit dem Schlüssel propKey eines Objekts obj zu definieren, verwenden wir eine Operation wie die folgende Methode

Object.defineProperty(obj, propKey, propDesc)

Diese Methode funktioniert je nachdem, wie die Eigenschaft aussieht, unterschiedlich

Das Hauptziel der Definition ist es also, eine eigene Eigenschaft zu erstellen (auch wenn ein vererbter Setter existiert, der ignoriert wird) und Eigenschaftsattribute zu ändern.

11.2 Zuweisung und Definition in der Theorie (optional)

  Eigenschaftsdeskriptoren in der ECMAScript-Spezifikation

In der Spezifikation sind Eigenschaftsdeskriptoren keine JavaScript-Objekte, sondern Records, eine speicherinterne Datenstruktur mit Fields. Die Schlüssel von Feldern werden in doppelten Klammern geschrieben. Zum Beispiel greift Desc.[[Configurable]] auf das Feld .[[Configurable]] von Desc zu. Diese Records werden beim Austausch mit der Außenwelt in JavaScript-Objekte übersetzt und umgekehrt.

11.2.1 Zuweisung zu einer Eigenschaft

Die eigentliche Arbeit der Zuweisung zu einer Eigenschaft wird über die folgende Operation in der ECMAScript-Spezifikation gehandhabt

OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc)

Dies sind die Parameter

Der Rückgabewert ist ein Boolean, der angibt, ob die Operation erfolgreich war oder nicht. Wie später in diesem Kapitel erklärt wird, wirft eine Zuweisung im Strict Mode einen TypeError, wenn OrdinarySetWithOwnDescriptor() fehlschlägt.

Dies ist eine grobe Zusammenfassung des Algorithmus

Im Detail funktioniert dieser Algorithmus wie folgt

11.2.1.1 Wie kommen wir von einer Zuweisung zu OrdinarySetWithOwnDescriptor()?

Die Auswertung einer Zuweisung ohne Destrukturierung umfasst die folgenden Schritte

Insbesondere wirft PutValue() im Strict Mode einen TypeError, wenn das Ergebnis von .[[Set]]() false ist.

11.2.2 Definition einer Eigenschaft

Die eigentliche Arbeit der Definition einer Eigenschaft wird über die folgende Operation in der ECMAScript-Spezifikation gehandhabt

ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current)

Die Parameter sind:

Das Ergebnis der Operation ist ein Boolean, der angibt, ob sie erfolgreich war. Ein Fehlschlag kann unterschiedliche Konsequenzen haben. Einige Aufrufer ignorieren das Ergebnis. Andere, wie Object.defineProperty(), werfen eine Ausnahme, wenn das Ergebnis false ist.

Dies ist eine Zusammenfassung des Algorithmus

11.3 Definition und Zuweisung in der Praxis

Dieser Abschnitt beschreibt einige Konsequenzen der Funktionsweise von Eigenschaftsdefinition und -zuweisung.

11.3.1 Nur die Definition erlaubt uns, eine Eigenschaft mit beliebigen Attributen zu erstellen

Wenn wir eine eigene Eigenschaft per Zuweisung erstellen, werden immer Eigenschaften mit den Attributen writable, enumerable und configurable erstellt, die alle true sind.

const obj = {};
obj.dataProp = 'abc';
assert.deepEqual(
  Object.getOwnPropertyDescriptor(obj, 'dataProp'),
  {
    value: 'abc',
    writable: true,
    enumerable: true,
    configurable: true,
  });

Wenn wir also beliebige Attribute angeben möchten, müssen wir die Definition verwenden.

Und obwohl wir Getter und Setter innerhalb von Objekt-Literalen erstellen können, können wir sie nicht später per Zuweisung hinzufügen. Auch hier benötigen wir die Definition.

11.3.2 Der Zuweisungsoperator ändert keine Eigenschaften in Prototypen

Betrachten wir das folgende Setup, bei dem obj die Eigenschaft prop von proto erbt.

const proto = { prop: 'a' };
const obj = Object.create(proto);

Wir können proto.prop nicht (destruktiv) ändern, indem wir obj.prop zuweisen. Dies erstellt eine neue eigene Eigenschaft

assert.deepEqual(
  Object.keys(obj), []);

obj.prop = 'b';

// The assignment worked:
assert.equal(obj.prop, 'b');

// But we created an own property and overrode proto.prop,
// we did not change it:
assert.deepEqual(
  Object.keys(obj), ['prop']);
assert.equal(proto.prop, 'a');

Die Begründung für dieses Verhalten ist folgende: Prototypen können Eigenschaften haben, deren Werte von allen ihren Nachkommen gemeinsam genutzt werden. Wenn wir eine solche Eigenschaft nur in einem Nachkommen ändern möchten, müssen wir dies nicht-destruktiv tun, durch Überschreiben. Dann beeinträchtigt die Änderung die anderen Nachkommen nicht.

11.3.3 Setter und Zuweisung vs. Definition

Was ist der Unterschied zwischen der Definition der Eigenschaft .prop von obj und der Zuweisung zu ihr?

Wenn wir definieren, ist unsere Absicht, entweder eine eigene (nicht vererbte) Eigenschaft von obj zu erstellen oder zu ändern. Daher ignoriert die Definition den vererbten Setter für .prop im folgenden Beispiel

let setterWasCalled = false;
const proto = {
  get prop() {
    return 'protoGetter';
  },
  set prop(x) {
    setterWasCalled = true;
  },
};
const obj = Object.create(proto);

assert.equal(obj.prop, 'protoGetter');

// Defining obj.prop:
Object.defineProperty(
  obj, 'prop', { value: 'objData' });
assert.equal(setterWasCalled, false);

// We have overridden the getter:
assert.equal(obj.prop, 'objData');

Wenn wir stattdessen .prop zuweisen, ist unsere Absicht oft, etwas zu ändern, das bereits existiert, und diese Änderung sollte vom Setter behandelt werden

let setterWasCalled = false;
const proto = {
  get prop() {
    return 'protoGetter';
  },
  set prop(x) {
    setterWasCalled = true;
  },
};
const obj = Object.create(proto);

assert.equal(obj.prop, 'protoGetter');

// Assigning to obj.prop:
obj.prop = 'objData';
assert.equal(setterWasCalled, true);

// The getter still active:
assert.equal(obj.prop, 'protoGetter');

11.3.4 Vererbte schreibgeschützte Eigenschaften verhindern die Erstellung eigener Eigenschaften per Zuweisung

Was passiert, wenn .prop in einem Prototyp schreibgeschützt ist?

const proto = Object.defineProperty(
  {}, 'prop', {
    value: 'protoValue',
    writable: false,
  });

In jedem Objekt, das das schreibgeschützte .prop von proto erbt, können wir per Zuweisung keine eigene Eigenschaft mit demselben Schlüssel erstellen – zum Beispiel

const obj = Object.create(proto);
assert.throws(
  () => obj.prop = 'objValue',
  /^TypeError: Cannot assign to read only property 'prop'/);

Warum können wir nicht zuweisen? Die Begründung ist, dass das Überschreiben einer vererbten Eigenschaft durch Erstellen einer eigenen Eigenschaft als nicht-destruktive Änderung der vererbten Eigenschaft angesehen werden kann. Man könnte argumentieren, dass wir das nicht tun sollten, wenn eine Eigenschaft nicht beschreibbar ist.

Die Definition von .prop funktioniert jedoch weiterhin und ermöglicht uns, zu überschreiben

Object.defineProperty(
  obj, 'prop', { value: 'objValue' });
assert.equal(obj.prop, 'objValue');

Accessor-Eigenschaften ohne Setter gelten ebenfalls als schreibgeschützt

const proto = {
  get prop() {
    return 'protoValue';
  }
};
const obj = Object.create(proto);
assert.throws(
  () => obj.prop = 'objValue',
  /^TypeError: Cannot set property prop of #<Object> which has only a getter$/);

  Der „Override-Fehler“: Vor- und Nachteile

Die Tatsache, dass schreibgeschützte Eigenschaften die Zuweisung weiter oben in der Prototypenkette verhindern, wurde als Override-Fehler bezeichnet.

11.4 Welche Sprachkonstrukte verwenden Definition, welche Zuweisung?

In diesem Abschnitt untersuchen wir, wo die Sprache Definition und wo sie Zuweisung verwendet. Wir erkennen, welche Operation verwendet wird, indem wir verfolgen, ob vererbte Setter aufgerufen werden oder nicht. Weitere Informationen finden Sie in §11.3.3 „Setter und Zuweisung vs. Definition“.

11.4.1 Die Eigenschaften eines Objekt-Literals werden per Definition hinzugefügt

Wenn wir Eigenschaften über ein Objekt-Literal erstellen, verwendet JavaScript immer die Definition (und ruft daher niemals vererbte Setter auf)

let lastSetterArgument;
const proto = {
  set prop(x) {
    lastSetterArgument = x;
  },
};
const obj = {
  __proto__: proto,
  prop: 'abc',
};
assert.equal(lastSetterArgument, undefined);

11.4.2 Der Zuweisungsoperator = verwendet immer Zuweisung

Der Zuweisungsoperator = verwendet immer die Zuweisung, um Eigenschaften zu erstellen oder zu ändern.

let lastSetterArgument;
const proto = {
  set prop(x) {
    lastSetterArgument = x;
  },
};
const obj = Object.create(proto);

// Normal assignment:
obj.prop = 'abc';
assert.equal(lastSetterArgument, 'abc');

// Assigning via destructuring:
[obj.prop] = ['def'];
assert.equal(lastSetterArgument, 'def');

11.4.3 Öffentliche Klassenfelder werden per Definition hinzugefügt

Leider verwenden öffentliche Klassenfelder, obwohl sie die gleiche Syntax wie Zuweisungen haben, *nicht* die Zuweisung zur Erstellung von Eigenschaften, sondern die Definition (wie Eigenschaften in Objekt-Literalen)

let lastSetterArgument1;
let lastSetterArgument2;
class A {
  set prop1(x) {
    lastSetterArgument1 = x;
  }
  set prop2(x) {
    lastSetterArgument2 = x;
  }
}
class B extends A {
  prop1 = 'one';
  constructor() {
    super();
    this.prop2 = 'two';
  }
}
new B();

// The public class field uses definition:
assert.equal(lastSetterArgument1, undefined);
// Inside the constructor, we trigger assignment:
assert.equal(lastSetterArgument2, 'two');

11.5 Weitere Lektüre und Quellen dieses Kapitels