Deep JavaScript
この書籍へのご支援をお願いします: 購入 または 寄付
(広告です。ブロックしないでください。)

11 プロパティ:代入 vs. 定義



オブジェクト obj のプロパティ prop を作成または変更する方法は2つあります。

この章では、それらがどのように機能するかを説明します。

  必要な知識: プロパティ属性とプロパティ記述子

この章では、プロパティ属性とプロパティ記述子に精通している必要があります。もしそうでなければ、§9 「プロパティ属性:入門」を確認してください。

11.1 代入 vs. 定義

11.1.1 代入

代入演算子 = を使用して、オブジェクト obj のプロパティ .prop に値 value を代入します。

obj.prop = value

この演算子は、.prop がどのように見えるかによって動作が異なります。

つまり、代入の主な目的は変更を加えることです。そのため、セッターをサポートしています。

11.1.2 定義

オブジェクト obj のキー propKey を持つプロパティを定義するには、次のメソッドなどの操作を使用します。

Object.defineProperty(obj, propKey, propDesc)

このメソッドは、プロパティがどのように見えるかによって動作が異なります。

つまり、定義の主な目的は(継承されたセッターがある場合でも無視して)独自のプロパティを作成し、プロパティ属性を変更することです。

11.2 理論における代入と定義(オプション)

  ECMAScript仕様のプロパティ記述子

仕様操作では、プロパティ記述子はJavaScriptオブジェクトではなく、レコードフィールドを持つ仕様内部のデータ構造)です。フィールドのキーは二重角括弧で記述されます。たとえば、Desc.[[Configurable]]Desc のフィールド .[[Configurable]] にアクセスします。これらのレコードは、外部とやり取りするときにJavaScriptオブジェクトとの間で変換されます。

11.2.1 プロパティへの代入

プロパティへの代入の実際の作業は、ECMAScript仕様の次の操作を通じて処理されます。

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

これらはパラメータです。

戻り値は、操作が成功したかどうかを示すブール値です。この章の後半で説明するように、厳格モードの代入は OrdinarySetWithOwnDescriptor() が失敗した場合に TypeError をスローします。

これはアルゴリズムの概要です。

詳細には、このアルゴリズムは次のように動作します。

11.2.1.1 どのようにして代入から OrdinarySetWithOwnDescriptor() にたどり着くのか?

分割代入なしの代入の評価には、次の手順が含まれます。

特に、PutValue() は、.[[Set]]() の結果が false の場合、厳格モードで TypeError をスローします。

11.2.2 プロパティの定義

プロパティの定義の実際の作業は、ECMAScript仕様の次の操作を通じて処理されます。

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

パラメータは次のとおりです。

操作の結果は、成功したかどうかを示すブール値です。失敗にはさまざまな結果が伴う可能性があります。一部の呼び出し元は結果を無視します。Object.defineProperty() などの他の呼び出し元は、結果が false の場合に例外をスローします。

これはアルゴリズムの概要です。

11.3 実際における定義と代入

このセクションでは、プロパティの定義と代入がどのように機能するかの結果について説明します。

11.3.1 定義のみが、任意の属性を持つプロパティの作成を可能にする

代入によって独自のプロパティを作成する場合、常にwritableenumerable、およびconfigurableの属性がすべてtrueであるプロパティを作成します。

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

したがって、任意の属性を指定したい場合は、定義を使用する必要があります。

また、オブジェクトリテラル内でゲッターとセッターを作成できますが、代入を介して後から追加することはできません。ここでも、定義が必要です。

11.3.2 代入演算子はプロトタイプのプロパティを変更しない

objprotoからプロパティpropを継承する次の設定を考えてみましょう。

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

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つの子孫でのみそのようなプロパティを変更したい場合は、オーバーライドを介して非破壊的に変更する必要があります。これにより、変更は他の子孫に影響を与えません。

11.3.3 代入はセッターを呼び出し、定義は呼び出さない

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

11.3.4 継承された読み取り専用プロパティは、代入による独自のプロパティの作成を妨げる

プロトタイプで.propが読み取り専用の場合、どうなりますか?

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

読み取り専用の.propprotoから継承するオブジェクトでは、代入を使用して、同じキーを持つ独自のプロパティを作成することはできません。たとえば、

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

なぜ代入できないのでしょうか?その根拠は、独自のプロパティを作成することによって継承されたプロパティをオーバーライドすることは、継承されたプロパティを非破壊的に変更することと見なせるからです。おそらく、プロパティが書き込み不可である場合、そうすることはできないはずです。

ただし、.propの定義は依然として機能し、オーバーライドを許可します。

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

セッターを持たないアクセサプロパティも、読み取り専用と見なされます。

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.4 どの言語構造が定義を使用し、どれが代入を使用するか?

このセクションでは、言語がどこで定義を使用し、どこで代入を使用するかを調べます。継承されたセッターが呼び出されるかどうかを追跡することにより、どの操作が使用されているかを検出します。詳細については、§11.3.3「代入はセッターを呼び出し、定義は呼び出さない」を参照してください。

11.4.1 オブジェクトリテラルのプロパティは定義を介して追加される

オブジェクトリテラルを介してプロパティを作成する場合、JavaScriptは常に定義を使用します(したがって、継承されたセッターを呼び出すことはありません)。

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

11.4.2 代入演算子=は常に代入を使用する

代入演算子=は、プロパティを作成または変更するために常に代入を使用します。

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 パブリッククラスフィールドは定義を介して追加される

残念ながら、パブリッククラスフィールドは代入と同じ構文を持っていますが、プロパティを作成するために代入を使用しません。オブジェクトリテラルのプロパティのように、定義を使用します。

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 この章の参考文献とソース