Es gibt zwei Arten, eine Eigenschaft prop eines Objekts obj zu erstellen oder zu ändern
obj.prop = trueObject.defineProperty(obj, '', {value: true})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“.
Wir verwenden den Zuweisungsoperator =, um einer Eigenschaft .prop eines Objekts obj einen Wert value zuzuweisen
Dieser Operator funktioniert je nachdem, wie .prop aussieht, unterschiedlich
Ändern von Eigenschaften: Wenn es eine eigene Daten-Eigenschaft .prop gibt, ändert die Zuweisung ihren Wert auf value.
Aufrufen von Settern: Wenn es einen eigenen oder vererbten Setter für .prop gibt, ruft die Zuweisung diesen Setter auf.
Erstellen von Eigenschaften: Wenn es keine eigene Daten-Eigenschaft .prop und keinen eigenen oder vererbten Setter dafür gibt, erstellt die Zuweisung eine neue eigene Daten-Eigenschaft.
Das Hauptziel der Zuweisung ist also, Änderungen vorzunehmen. Deshalb unterstützt sie Setter.
Um eine Eigenschaft mit dem Schlüssel propKey eines Objekts obj zu definieren, verwenden wir eine Operation wie die folgende Methode
Diese Methode funktioniert je nachdem, wie die Eigenschaft aussieht, unterschiedlich
propKey existiert, ändert die Definition ihre Eigenschaftsattribute gemäß dem Eigenschaftsdeskriptor propDesc (falls möglich).propDesc angibt (falls möglich).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.
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.
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
O ist das Objekt, das gerade besucht wird.P ist der Schlüssel der Eigenschaft, der wir etwas zuweisen.V ist der Wert, den wir zuweisen.Receiver ist das Objekt, bei dem die Zuweisung begonnen hat.ownDesc ist der Deskriptor von O[P] oder null, wenn diese Eigenschaft nicht existiert.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
Receiver, bis er eine Eigenschaft mit dem Schlüssel P findet. Der Durchlauf erfolgt durch rekursives Aufrufen von OrdinarySetWithOwnDescriptor(). Während der Rekursion ändert sich O und zeigt auf das aktuell besuchte Objekt, aber Receiver bleibt unverändert.Receiver (wo die Rekursion begann) erstellt oder etwas anderes geschieht.Im Detail funktioniert dieser Algorithmus wie folgt
ownDesc undefined ist, haben wir bisher keine Eigenschaft mit dem Schlüssel P gefundenWenn O einen Prototyp parent hat, geben wir parent.[[Set]](P, V, Receiver) zurück. Dies setzt unsere Suche fort. Der Methodenaufruf führt normalerweise zu einem rekursiven Aufruf von OrdinarySetWithOwnDescriptor().
Andernfalls ist unsere Suche nach P fehlgeschlagen und wir setzen ownDesc wie folgt
{
[[Value]]: undefined, [[Writable]]: true,
[[Enumerable]]: true, [[Configurable]]: true
}
Mit diesem ownDesc erstellt die nächste if-Anweisung eine eigene Eigenschaft in Receiver.
ownDesc eine Daten-Eigenschaft spezifiziert, dann haben wir eine Eigenschaft gefundenownDesc.[[Writable]] false ist, geben wir false zurück. Das bedeutet, dass jede nicht beschreibbare Eigenschaft P (eigen oder vererbt!) die Zuweisung verhindert.existingDescriptor Receiver.[[GetOwnProperty]](P). Das heißt, wir rufen den Deskriptor der Eigenschaft ab, bei der die Zuweisung begann. Wir haben jetztO und den aktuellen Eigenschaftsdeskriptor ownDesc einerseits.Receiver und den ursprünglichen Eigenschaftsdeskriptor existingDescriptor andererseits.existingDescriptor nicht undefined istReceiver keine Eigenschaft P hat.)if-Bedingungen sollten niemals true sein, da ownDesc und existingDesc gleich sein solltenexistingDescriptor einen Accessor spezifiziert, geben wir false zurück.existingDescriptor.[[Writable]] false ist, geben wir false zurück.Receiver.[[DefineOwnProperty]](P, { [[Value]]: V }) zurück. Diese interne Methode führt eine Definition durch, die wir verwenden, um den Wert der Eigenschaft Receiver[P] zu ändern. Der Definitionsalgorithmus wird im nächsten Unterabschnitt beschrieben.Receiver keine eigene Eigenschaft mit dem Schlüssel P.)CreateDataProperty(Receiver, P, V) zurück. (Diese Operation erstellt eine eigene Daten-Eigenschaft im ersten Argument.)ownDesc eine eigene oder vererbte Accessor-Eigenschaft.)setter ownDesc.[[Set]].setter undefined ist, geben wir false zurück.Call(setter, Receiver, «V») aus. Call() ruft das Funktions-Objekt setter auf, wobei this auf Receiver gesetzt ist und der einzige Parameter V ist (französische Anführungszeichen «» werden in der Spezifikation für Listen verwendet).true zurück.OrdinarySetWithOwnDescriptor()?Die Auswertung einer Zuweisung ohne Destrukturierung umfasst die folgenden Schritte
AssignmentExpression. Dieser Abschnitt behandelt die Bereitstellung von Namen für anonyme Funktionen, Destrukturierung und mehr.PutValue() verwendet, um die Zuweisung vorzunehmen.PutValue() die interne Methode .[[Set]]() auf..[[Set]]() OrdinarySet() auf (was OrdinarySetWithOwnDescriptor() aufruft) und gibt das Ergebnis zurück.Insbesondere wirft PutValue() im Strict Mode einen TypeError, wenn das Ergebnis von .[[Set]]() false ist.
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:
O, bei dem wir eine Eigenschaft definieren wollen. Es gibt einen speziellen Modus zur Validierung, bei dem O undefined ist. Diesen Modus ignorieren wir hier.P, die wir definieren wollen.extensible gibt an, ob O erweiterbar ist.Desc ist ein Eigenschaftsdeskriptor, der die gewünschten Attribute der Eigenschaft angibt.current enthält den Eigenschaftsdeskriptor einer eigenen Eigenschaft O[P], falls sie existiert. Andernfalls ist current undefined.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
Wenn current undefined ist, dann existiert die Eigenschaft P derzeit nicht und muss erstellt werden.
extensible false ist, geben wir false zurück, was anzeigt, dass die Eigenschaft nicht hinzugefügt werden konnte.Desc und erstellen entweder eine Daten-Eigenschaft oder eine Accessor-Eigenschaft.true zurück.Wenn Desc keine Felder hat, geben wir true zurück, was anzeigt, dass die Operation erfolgreich war (da keine Änderungen vorgenommen werden mussten).
Wenn current.[[Configurable]] false ist
Desc darf keine anderen Attribute als value ändern.)Desc.[[Configurable]] existiert, muss es denselben Wert wie current.[[Configurable]] haben. Wenn nicht, geben wir false zurück.Desc.[[Enumerable]]Als Nächstes **validieren** wir den Eigenschaftsdeskriptor Desc: Können die von current beschriebenen Attribute auf die von Desc spezifizierten Werte geändert werden? Wenn nicht, geben wir false zurück. Wenn ja, fahren wir fort.
false zurückgegeben..[[Configurable]] und .[[Enumerable]] beibehalten, alle anderen Attribute erhalten Standardwerte (undefined für objektwertige Attribute, false für boolesche Attribute).current.[[Configurable]] als auch current.[[Writable]] false sind, sind keine Änderungen erlaubt und Desc und current müssen dieselben Attribute spezifizierencurrent.[[Configurable]] als false wurden Desc.[[Configurable]] und Desc.[[Enumerable]] bereits zuvor geprüft und haben die richtigen Werte.)Desc.[[Writable]] existiert und true ist, geben wir false zurück.Desc.[[Value]] existiert und nicht denselben Wert wie current.[[Value]] hat, geben wir false zurück.true zurück, was anzeigt, dass der Algorithmus erfolgreich war.current.[[Configurable]] false ist, sind keine Änderungen erlaubt und Desc und current müssen dieselben Attribute spezifizierencurrent.[[Configurable]] als false wurden Desc.[[Configurable]] und Desc.[[Enumerable]] bereits zuvor geprüft und haben die richtigen Werte.)Desc.[[Set]] existiert, muss es denselben Wert wie current.[[Set]] haben. Wenn nicht, geben wir false zurück.Desc.[[Get]]true zurück, was anzeigt, dass der Algorithmus erfolgreich war.Setzen Sie die Attribute der Eigenschaft mit dem Schlüssel P auf die von Desc spezifizierten Werte. Aufgrund der Validierung können wir sicher sein, dass alle Änderungen erlaubt sind.
Geben Sie true zurück.
Dieser Abschnitt beschreibt einige Konsequenzen der Funktionsweise von Eigenschaftsdefinition und -zuweisung.
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.
Betrachten wir das folgende Setup, bei dem obj die Eigenschaft prop von proto erbt.
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.
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');Was passiert, wenn .prop in einem Prototyp schreibgeschützt ist?
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
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.
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“.
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);= verwendet immer ZuweisungDer 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');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');Abschnitt „Prototype chains“ in „JavaScript for impatient programmers“
E-Mail von Allen Wirfs-Brock an die es-discuss-Mailingliste: „The distinction between assignment and definition […] was not very important when all ES had was data properties and there was no way for ES code to manipulate property attributes.“ [Das änderte sich mit ECMAScript 5.]