=
は常に代入を使用するオブジェクト obj
のプロパティ prop
を作成または変更する方法は2つあります。
obj.prop = true
Object.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で変わりました。]」