プロパティディスクリプター

プロパティディスクリプターには2つのタイプがあります。

  1. データディスクリプター
  2. アクセサディスクリプター

データディスクリプター

以下のようなオブジェクトを定義する場合で考えます。

const object = {
  foo: 1,
};

この定義方法は以下と同等です。

const object = {};

Object.defineProperty(object, 'foo', {
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true,
});

オブジェクトの各プロパティは値の他にwritableenumerableconfigurableなどの詳細情報を持ちます。

writable

writableではそのキーの値を更新できるかを決めます。trueだと変更可能でObject#definePropertyで値を渡さなかった時はfalseが設定されます。また、falseの時に値を更新しようとするとTypeErrorが起きます。

const object = {};

Object.defineProperty(object, 'a', {
  value: 1,
  writable: false,
});

object.a = 2;
// TypeError
// Cannot assign to read only property 'a' of object '#<Object>'

あるオブジェクトのすべてのプロパティのwritablefalseにしたい時、Object#freezeメソッドが使えます。これはconfigurablefalseに設定し、オブジェクトが新しいプロパティを追加できなくするので注意が要ります。

Object.freeze(object);
// Object.isFrozen(object);

enumerable

enumerableではキーを巡回する時に、そのキーも巡回対象に含めるかを決めます。trueだとObject#keysの結果の配列に含まれたり、for-in構文で回るキーに含まれたりします。 Object#definePropertyで値を渡さなかった時はfalseが設定されます。

const object = {a: 1};

Object.defineProperty(object, 'b', {
  value: 2,
  enumerable: true,
});

console.log(Object.keys(object));
// ["a", "b"]

for (const key in object) {
  console.log(key);
}
// "a"
// "b"

configurable

configurableではそのディスクリプターを更新できるかを決めます。Object#definePropertyで値を渡さなかった時はfalseが設定されます。また、falseの時に値を更新しようとするとTypeErrorが起きます。

const object = {a: 1};

Object.defineProperty(object, 'b', {
  value: 2,
  configurable: false,
});

Object.defineProperty(object, 'b', {
  value: 3,
});
// TypeError
// Cannot redefine property: b

更新できなくしたプロパティは削除などもできなくなります。

delete object.b;
object.b; // 2

configurabletrueからfalseに更新したい場合は、同じプロパティに対して再度definePropertyを使います。

Object.defineProperty(object, 'b', {
  value: 2,
  configurable: false,
});

またすべてのconfigurablefalseにしたい場合は、Object#sealObject#freezeメソッドが使えます。Object#freezewritablefalseにします。
注意点としてこれらのメソッドを適用したオブジェクトはプロパティの追加もできなくなります。

Object.seal(object);
// Object.isSealed(object);

アクセサディスクリプター

データディスクリプターのvaluewritableの代わりにgetsetの項目が含まれます。このgetsetはそれぞれゲッターとセッターとなる関数であり、アクセサディスクリプターはゲッターとセッターに関する詳細設定を行います。

以下のようなオブジェクトを定義する場合で考えます。

const object = {
  get foo() {
    return this._foo;
  },
  set foo(value) {
    this._foo = value;
  },
};

この定義方法は以下と同等です。

const object = {};

Object.defineProperty(object, 'foo', {
  get() {
    return this._foo;
  },
  set(value) {
    this._foo = value;
  },
  enumerable: true,
  configurable: true,
});

この中でenumerableconfigurableはデータディスクリプターのそれをまったく同じ使い方をします。

セッターを設定しないと、それを使おうとした時にTypeErrorが起きます。

const object = {};

Object.defineProperty(object, 'foo', {
  get() {
    return this._foo;
  },
  // `set: undefined` と同じ
  enumerable: true,
  configurable: true,
});

object.foo = '...';
// TypeError
// Cannot set property foo of #<Object> which has only a getter

対してゲッターは設定しないとしてもundefinedとして使えます。

const object = {};

Object.defineProperty(object, 'foo', {
  // `get: undefined` と同じ
  set(value) {
    this._foo = value;
  },
  enumerable: true,
  configurable: true,
});

object.foo // undefined

ディスクリプターの取得

1つだけならObject#getOwnPropertyDescriptor、すべてならObject#getOwnPropertyDescriptorsで取得できます。ちなみにObject#getOwnPropertyDescriptorsで取得する値に関してenumerableの影響は受けず常に取得します。

Object#getOwnPropertyDescriptor

第1引数にオブジェクト、第2引数に取得したいキーを渡します。

Object.getOwnPropertyDescriptor(
  object,
  'foo',
);
// {value: ...}

Object#getOwnPropertyDescriptors

第1引数にオブジェクトを渡すだけです。各値がディスクリプターとなったオブジェクトが帰ります。

Object.getOwnPropertyDescriptors(
  object,
);
// {foo: ...}