=は常に代入を使用するオブジェクト obj のプロパティ prop を作成または変更する方法は2つあります。
obj.prop = trueObject.defineProperty(obj, '', {value: true})この章では、それらがどのように機能するかを説明します。
必要な知識: プロパティ属性とプロパティ記述子
この章では、プロパティ属性とプロパティ記述子に精通している必要があります。もしそうでなければ、§9 「プロパティ属性:入門」を確認してください。
代入演算子 = を使用して、オブジェクト obj のプロパティ .prop に値 value を代入します。
この演算子は、.prop がどのように見えるかによって動作が異なります。
プロパティの変更:独自のデータプロパティ .prop がある場合、代入はその値を value に変更します。
セッターの呼び出し:独自のセッターまたは継承されたセッターが .prop にある場合、代入はそのセッターを呼び出します。
プロパティの作成:独自のデータプロパティ .prop がなく、独自のセッターまたは継承されたセッターもない場合、代入は新しい独自のデータプロパティを作成します。
つまり、代入の主な目的は変更を加えることです。そのため、セッターをサポートしています。
オブジェクト obj のキー propKey を持つプロパティを定義するには、次のメソッドなどの操作を使用します。
このメソッドは、プロパティがどのように見えるかによって動作が異なります。
propKey を持つ独自のプロパティが存在する場合、定義はそのプロパティ記述子 propDesc で指定されたプロパティ属性(可能な場合)を変更します。propDesc で指定された属性を持つ独自のプロパティを作成します(可能な場合)。つまり、定義の主な目的は(継承されたセッターがある場合でも無視して)独自のプロパティを作成し、プロパティ属性を変更することです。
ECMAScript仕様のプロパティ記述子
仕様操作では、プロパティ記述子はJavaScriptオブジェクトではなく、レコード(フィールドを持つ仕様内部のデータ構造)です。フィールドのキーは二重角括弧で記述されます。たとえば、Desc.[[Configurable]] は Desc のフィールド .[[Configurable]] にアクセスします。これらのレコードは、外部とやり取りするときにJavaScriptオブジェクトとの間で変換されます。
プロパティへの代入の実際の作業は、ECMAScript仕様の次の操作を通じて処理されます。
OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc)
これらはパラメータです。
O は現在訪問中のオブジェクトです。P は代入対象のプロパティのキーです。V は代入する値です。Receiver は代入が開始されたオブジェクトです。ownDesc は O[P] の記述子、またはプロパティが存在しない場合は null です。戻り値は、操作が成功したかどうかを示すブール値です。この章の後半で説明するように、厳格モードの代入は OrdinarySetWithOwnDescriptor() が失敗した場合に TypeError をスローします。
これはアルゴリズムの概要です。
P であるプロパティが見つかるまで、Receiver のプロトタイプチェーンをトラバースします。トラバースは、OrdinarySetWithOwnDescriptor() を再帰的に呼び出すことによって行われます。再帰中、O が変化し、現在訪問中のオブジェクトを指しますが、Receiver は同じままです。Receiver (再帰が開始された場所)に作成されるか、その他の何かが発生します。詳細には、このアルゴリズムは次のように動作します。
ownDesc が undefined の場合、キー P を持つプロパティはまだ見つかっていません。O にプロトタイプ parent がある場合、parent.[[Set]](P, V, Receiver) を返します。これにより、検索が続行されます。メソッド呼び出しは通常、OrdinarySetWithOwnDescriptor() を再帰的に呼び出すことになります。
それ以外の場合、P の検索が失敗し、ownDesc を次のように設定します。
{
[[Value]]: undefined, [[Writable]]: true,
[[Enumerable]]: true, [[Configurable]]: true
}
この ownDesc を使用して、次の if ステートメントは Receiver に独自のプロパティを作成します。
ownDesc がデータプロパティを指定する場合、プロパティが見つかりました。ownDesc.[[Writable]] が false の場合、false を返します。これは、書き込み不可のプロパティ P (独自または継承されたもの!)が代入を妨げることを意味します。existingDescriptor を Receiver.[[GetOwnProperty]](P) とします。つまり、代入が開始された場所のプロパティの記述子を取得します。これで、以下ができました。O と現在のプロパティ記述子 ownDesc。Receiver と元のプロパティ記述子 existingDescriptor。existingDescriptor が undefined でない場合Receiver にプロパティ P がない場合にのみ再帰します。)if 条件は、ownDesc と existingDesc が等しくなるため、true になることはありません。existingDescriptor がアクセサーを指定する場合、false を返します。existingDescriptor.[[Writable]] が false の場合、false を返します。Receiver.[[DefineOwnProperty]](P, { [[Value]]: V }) を返します。この内部メソッドは、プロパティ Receiver[P] の値を変更するために使用する定義を実行します。定義アルゴリズムについては、次のサブセクションで説明します。Receiver にキー P を持つ独自のプロパティはありません。)CreateDataProperty(Receiver, P, V) を返します。(この操作は、最初の引数に独自のデータプロパティを作成します。)ownDesc は独自のまたは継承されたアクセサープロパティを記述します。)setter を ownDesc.[[Set]] とします。setter が undefined の場合、false を返します。Call(setter, Receiver, «V») を実行します。Call() は、this が Receiver に設定され、単一のパラメータ V を持つ関数オブジェクト setter を呼び出します(仕様では、フランス語の引用符 «» がリストに使用されます)。true を返します。OrdinarySetWithOwnDescriptor() にたどり着くのか?分割代入なしの代入の評価には、次の手順が含まれます。
AssignmentExpression のランタイムセマンティクスのセクションで開始されます。このセクションでは、匿名関数の名前付け、分割代入などを処理します。PutValue() を使用して代入を行います。PutValue() は内部メソッド .[[Set]]() を呼び出します。.[[Set]]() は OrdinarySet() (OrdinarySetWithOwnDescriptor() を呼び出す)を呼び出し、結果を返します。特に、PutValue() は、.[[Set]]() の結果が false の場合、厳格モードで TypeError をスローします。
プロパティの定義の実際の作業は、ECMAScript仕様の次の操作を通じて処理されます。
ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current)
パラメータは次のとおりです。
O。 O が undefined である特別な検証専用モードがあります。ここでは、このモードを無視しています。P。extensible は O が拡張可能かどうかを示します。Desc は、プロパティが持つようにしたい属性を指定するプロパティ記述子です。current には、独自のプロパティ O[P] が存在する場合、そのプロパティ記述子が含まれます。それ以外の場合、current は undefined です。操作の結果は、成功したかどうかを示すブール値です。失敗にはさまざまな結果が伴う可能性があります。一部の呼び出し元は結果を無視します。Object.defineProperty() などの他の呼び出し元は、結果が false の場合に例外をスローします。
これはアルゴリズムの概要です。
current が undefined の場合、プロパティ P は現在存在しないため、作成する必要があります。
extensible が false の場合、プロパティを追加できなかったことを示す false を返します。Desc を確認し、データプロパティまたはアクセサープロパティのいずれかを作成します。true を返します。Desc にフィールドがない場合、操作が成功したことを示す true を返します(変更を行う必要がなかったため)。
current.[[Configurable]] が false の場合
Desc は value 以外の属性を変更することを許可されていません。)Desc.[[Configurable]] が存在する場合は、current.[[Configurable]] と同じ値を持っている必要があります。そうでない場合は、false を返します。Desc.[[Enumerable]]次に、プロパティ記述子Descを検証します。currentで記述された属性をDescで指定された値に変更できるか?できない場合は、falseを返します。できる場合は、続行します。
falseが返されます。.[[Configurable]]と.[[Enumerable]]の値は保持され、他のすべての属性はデフォルト値(オブジェクト値の属性の場合はundefined、ブール値の属性の場合はfalse)になります。current.[[Configurable]]とcurrent.[[Writable]]の両方がfalseの場合、変更は許可されず、Descとcurrentは同じ属性を指定する必要があります。current.[[Configurable]]がfalseであるため、Desc.[[Configurable]]とDesc.[[Enumerable]]はすでに以前にチェックされており、正しい値を持っています。)Desc.[[Writable]]が存在し、trueである場合は、falseを返します。Desc.[[Value]]が存在し、current.[[Value]]と同じ値を持たない場合は、falseを返します。trueを返します。current.[[Configurable]]がfalseの場合、変更は許可されず、Descとcurrentは同じ属性を指定する必要があります。current.[[Configurable]]がfalseであるため、Desc.[[Configurable]]とDesc.[[Enumerable]]はすでに以前にチェックされており、正しい値を持っています。)Desc.[[Set]]が存在する場合は、current.[[Set]]と同じ値を持つ必要があります。そうでない場合は、falseを返します。Desc.[[Get]]trueを返します。キーPを持つプロパティの属性を、Descで指定された値に設定します。検証により、すべての変更が許可されていることが保証されます。
true を返します。
このセクションでは、プロパティの定義と代入がどのように機能するかの結果について説明します。
代入によって独自のプロパティを作成する場合、常にwritable、enumerable、およびconfigurableの属性がすべてtrueであるプロパティを作成します。
const obj = {};
obj.dataProp = 'abc';
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'dataProp'),
{
value: 'abc',
writable: true,
enumerable: true,
configurable: true,
});したがって、任意の属性を指定したい場合は、定義を使用する必要があります。
また、オブジェクトリテラル内でゲッターとセッターを作成できますが、代入を介して後から追加することはできません。ここでも、定義が必要です。
objがprotoからプロパティpropを継承する次の設定を考えてみましょう。
obj.propに代入しても、proto.propを(破壊的に)変更することはできません。そうすると、新しい独自のプロパティが作成されます。
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');この動作の根拠は次のとおりです。プロトタイプは、すべての子孫によって値が共有されるプロパティを持つことができます。1つの子孫でのみそのようなプロパティを変更したい場合は、オーバーライドを介して非破壊的に変更する必要があります。これにより、変更は他の子孫に影響を与えません。
objのプロパティ.propを定義することと、それに代入することの違いは何ですか?
定義する場合、私たちの意図は、objの独自の(継承されていない)プロパティを作成または変更することです。したがって、定義は、次の例で.propに対する継承されたセッターを無視します。
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');代わりに、.propに代入する場合、私たちの意図は、多くの場合、すでに存在する何かを変更することであり、その変更はセッターによって処理される必要があります。
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');プロトタイプで.propが読み取り専用の場合、どうなりますか?
読み取り専用の.propをprotoから継承するオブジェクトでは、代入を使用して、同じキーを持つ独自のプロパティを作成することはできません。たとえば、
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 'objValue',
/^TypeError: Cannot assign to read only property 'prop'/);なぜ代入できないのでしょうか?その根拠は、独自のプロパティを作成することによって継承されたプロパティをオーバーライドすることは、継承されたプロパティを非破壊的に変更することと見なせるからです。おそらく、プロパティが書き込み不可である場合、そうすることはできないはずです。
ただし、.propの定義は依然として機能し、オーバーライドを許可します。
セッターを持たないアクセサプロパティも、読み取り専用と見なされます。
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$/); 「オーバーライドの間違い」:長所と短所
読み取り専用プロパティがプロトタイプチェーンの早い段階で代入を妨げるという事実は、オーバーライドの間違いと呼ばれています。
このセクションでは、言語がどこで定義を使用し、どこで代入を使用するかを調べます。継承されたセッターが呼び出されるかどうかを追跡することにより、どの操作が使用されているかを検出します。詳細については、§11.3.3「代入はセッターを呼び出し、定義は呼び出さない」を参照してください。
オブジェクトリテラルを介してプロパティを作成する場合、JavaScriptは常に定義を使用します(したがって、継承されたセッターを呼び出すことはありません)。
let lastSetterArgument;
const proto = {
set prop(x) {
lastSetterArgument = x;
},
};
const obj = {
__proto__: proto,
prop: 'abc',
};
assert.equal(lastSetterArgument, undefined);=は常に代入を使用する代入演算子=は、プロパティを作成または変更するために常に代入を使用します。
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');残念ながら、パブリッククラスフィールドは代入と同じ構文を持っていますが、プロパティを作成するために代入を使用しません。オブジェクトリテラルのプロパティのように、定義を使用します。
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');Allen Wirfs-Brockによるes-discussメーリングリストへのメール:「代入と定義の区別[...]は、ESがデータプロパティしか持っておらず、ESコードがプロパティ属性を操作する方法がなかったときは、それほど重要ではありませんでした。[それはECMAScript 5で変わりました。]」