Deep JavaScript
この本のサポートをお願いします:購入する または 寄付する
(広告、ブロックしないでください。)

9 プロパティ属性:入門



この章では、ECMAScript 仕様が JavaScript オブジェクトをどのように見ているかについて詳しく見ていきます。特に、プロパティは仕様上アトミックではなく、複数の属性(レコードのフィールドと考えてください)で構成されています。データプロパティの値でさえ、属性に格納されます!

9.1 オブジェクトの構造

ECMAScript 仕様では、オブジェクトは以下で構成されます。

9.1.1 内部スロット

仕様は、内部スロットを次のように説明しています。箇条書きを追加し、一部を強調しました。

内部スロットには2種類あります。

通常のオブジェクトには、次のデータスロットがあります。

9.1.2 プロパティキー

プロパティのキーは次のいずれかです。

9.1.3 プロパティ属性

プロパティには2種類あり、それらは属性によって特徴付けられます。

さらに、両方の種類のプロパティが持つ属性があります。次の表に、すべての属性とそのデフォルト値を示します。

プロパティの種類 属性の名前と型 デフォルト値
データプロパティ value:任意 undefined
writable:ブール値 false
アクセサープロパティ get:(this: any) => any undefined
set:(this: any, v: any) => void undefined
すべてのプロパティ configurable:ブール値 false
enumerable:ブール値 false

すでに属性 valueget、および set については説明しました。その他の属性は次のように機能します。

9.1.3.1 落とし穴:継承された書き込み不可プロパティは、代入による自身のプロパティの作成を妨げる

継承されたプロパティが書き込み不可の場合、代入を使用して同じキーを持つ自身のプロパティを作成することはできません。

const proto = {
  prop: 1,
};
// Make proto.prop non-writable:
Object.defineProperty(
  proto, 'prop', {writable: false});

const obj = Object.create(proto);

assert.throws(
  () => obj.prop = 2,
  /^TypeError: Cannot assign to read only property 'prop'/);

詳細については、§11.3.4「継承された読み取り専用プロパティは、代入による自身のプロパティの作成を妨げる」を参照してください。

9.2 プロパティ記述子

プロパティ記述子は、プロパティの属性を JavaScript オブジェクトとしてエンコードします。それらの TypeScript インターフェースは次のようになります。

interface DataPropertyDescriptor {
  value?: any;
  writable?: boolean;
  configurable?: boolean;
  enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
  get?: (this: any) => any;
  set?: (this: any, v: any) => void;
  configurable?: boolean;
  enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;

疑問符は、すべてのプロパティがオプションであることを示します。§9.7「記述子プロパティの省略」では、それらを省略した場合に何が起こるかを説明しています。

9.3 プロパティの記述子の取得

9.3.1 Object.getOwnPropertyDescriptor():単一プロパティの記述子を取得する

次のオブジェクトを考えてみましょう

const legoBrick = {
  kind: 'Plate 1x3',
  color: 'yellow',
  get description() {
    return `${this.kind} (${this.color})`;
  },
};

最初に、データプロパティ .color の記述子を取得しましょう

assert.deepEqual(
  Object.getOwnPropertyDescriptor(legoBrick, 'color'),
  {
    value: 'yellow',
    writable: true,
    enumerable: true,
    configurable: true,
  });

これがアクセサープロパティ .description の記述子です。

const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(legoBrick, 'description'),
  {
    get: desc(legoBrick, 'description').get, // (A)
    set: undefined,
    enumerable: true,
    configurable: true
  });

A 行のユーティリティ関数 desc() を使用すると、.deepEqual() が確実に機能します。

9.3.2 Object.getOwnPropertyDescriptors():オブジェクトのすべてのプロパティの記述子を取得する

const legoBrick = {
  kind: 'Plate 1x3',
  color: 'yellow',
  get description() {
    return `${this.kind} (${this.color})`;
  },
};

const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(legoBrick),
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: desc(legoBrick, 'description').get, // (A)
      set: undefined,
      enumerable: true,
      configurable: true,
    },
  });

A 行のヘルパー関数 desc() を使用すると、.deepEqual() が確実に機能します。

9.4 記述子によるプロパティの定義

プロパティ記述子 propDesc を介してキー k を持つプロパティを定義する場合、何が起こるかは次のようになります。

9.4.1 Object.defineProperty():記述子による単一プロパティの定義

まず、記述子を使用して新しいプロパティを作成しましょう

const car = {};

Object.defineProperty(car, 'color', {
  value: 'blue',
  writable: true,
  enumerable: true,
  configurable: true,
});

assert.deepEqual(
  car,
  {
    color: 'blue',
  });

次に、記述子を介してプロパティの種類を変更します。データプロパティをゲッターに変えます

const car = {
  color: 'blue',
};

let readCount = 0;
Object.defineProperty(car, 'color', {
  get() {
    readCount++;
    return 'red';
  },
});

assert.equal(car.color, 'red');
assert.equal(readCount, 1);

最後に、記述子を介してデータプロパティの値を変更します

const car = {
  color: 'blue',
};

// Use the same attributes as assignment:
Object.defineProperty(
  car, 'color', {
    value: 'green',
    writable: true,
    enumerable: true,
    configurable: true,
  });

assert.deepEqual(
  car,
  {
    color: 'green',
  });

代入と同じプロパティ属性を使用しました。

9.4.2 Object.defineProperties():記述子による複数プロパティの定義

Object.defineProperties()Object.defineProperty() の複数プロパティバージョンです。

const legoBrick1 = {};
Object.defineProperties(
  legoBrick1,
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: function () {
        return `${this.kind} (${this.color})`;
      },
      enumerable: true,
      configurable: true,
    },
  });

assert.deepEqual(
  legoBrick1,
  {
    kind: 'Plate 1x3',
    color: 'yellow',
    get description() {
      return `${this.kind} (${this.color})`;
    },
  });

9.5 Object.create():記述子によるオブジェクトの作成

Object.create() は新しいオブジェクトを作成します。最初の引数はそのオブジェクトのプロトタイプを指定します。オプションの2番目の引数は、そのオブジェクトのプロパティの記述子を指定します。次の例では、前の例と同じオブジェクトを作成します。

const legoBrick2 = Object.create(
  Object.prototype,
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: function () {
        return `${this.kind} (${this.color})`;
      },
      enumerable: true,
      configurable: true,
    },
  });

// Did we really create the same object?
assert.deepEqual(legoBrick1, legoBrick2); // Yes!

9.6 Object.getOwnPropertyDescriptors() のユースケース

Object.getOwnPropertyDescriptors() は、Object.defineProperties() または Object.create() と組み合わせることで、2つのユースケースに役立ちます。

9.6.1 ユースケース:プロパティをオブジェクトにコピーする

ES6以降、JavaScriptにはプロパティをコピーするためのツールメソッド Object.assign() がすでにあります。ただし、このメソッドは、キーが key のプロパティをコピーするために、単純な get および set 操作を使用します。

target[key] = source[key];

つまり、プロパティの忠実なコピーを作成するのは、次の場合のみです。

次の例は、この制限事項を示しています。オブジェクトsourceには、キーがdataであるセッターがあります。

const source = {
  set data(value) {
    this._data = value;
  }
};

// Property `data` exists because there is only a setter
// but has the value `undefined`.
assert.equal('data' in source, true);
assert.equal(source.data, undefined);

Object.assign()を使用してプロパティdataをコピーすると、アクセサープロパティdataはデータプロパティに変換されます。

const target1 = {};
Object.assign(target1, source);

assert.deepEqual(
  Object.getOwnPropertyDescriptor(target1, 'data'),
  {
    value: undefined,
    writable: true,
    enumerable: true,
    configurable: true,
  });

// For comparison, the original:
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(source, 'data'),
  {
    get: undefined,
    set: desc(source, 'data').set,
    enumerable: true,
    configurable: true,
  });

幸いなことに、Object.getOwnPropertyDescriptors()Object.defineProperties()と組み合わせて使用すると、プロパティdataを忠実にコピーできます。

const target2 = {};
Object.defineProperties(
  target2, Object.getOwnPropertyDescriptors(source));

assert.deepEqual(
  Object.getOwnPropertyDescriptor(target2, 'data'),
  {
    get: undefined,
    set: desc(source, 'data').set,
    enumerable: true,
    configurable: true,
  });
9.6.1.1 落とし穴: superを使用するメソッドのコピー

superを使用するメソッドは、そのホームオブジェクト(それが格納されているオブジェクト)と密接に結びついています。現在、このようなメソッドを別のオブジェクトにコピーまたは移動する方法はありません。

9.6.2 Object.getOwnPropertyDescriptors()のユースケース: オブジェクトのクローン

浅いクローン作成はプロパティのコピーに似ているため、ここでもObject.getOwnPropertyDescriptors()が適しています。

クローンを作成するには、Object.create()を使用します。

const original = {
  set data(value) {
    this._data = value;
  }
};

const clone = Object.create(
  Object.getPrototypeOf(original),
  Object.getOwnPropertyDescriptors(original));

assert.deepEqual(original, clone);

このトピックの詳細については、§6「オブジェクトと配列のコピー」を参照してください。

9.7 ディスクリプタプロパティの省略

ディスクリプタのすべてのプロパティはオプションです。プロパティを省略した場合に何が起こるかは、操作によって異なります。

9.7.1 プロパティ作成時のディスクリプタプロパティの省略

ディスクリプタを介して新しいプロパティを作成する場合、属性を省略すると、それらのデフォルト値が使用されます。

const car = {};
Object.defineProperty(
  car, 'color', {
    value: 'red',
  });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'red',
    writable: false,
    enumerable: false,
    configurable: false,
  });

9.7.2 プロパティ変更時のディスクリプタプロパティの省略

代わりに、既存のプロパティを変更する場合、ディスクリプタプロパティを省略すると、対応する属性は変更されません。

const car = {
  color: 'yellow',
};
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'yellow',
    writable: true,
    enumerable: true,
    configurable: true,
  });
Object.defineProperty(
  car, 'color', {
    value: 'pink',
  });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'pink',
    writable: true,
    enumerable: true,
    configurable: true,
  });

9.8 組み込み構文はどのプロパティ属性を使用するか?

プロパティ属性の一般的なルールは(いくつかの例外を除いて)、次のとおりです。

9.8.1 代入によって作成された独自のプロパティ

const obj = {};
obj.prop = 3;

assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    prop: {
      value: 3,
      writable: true,
      enumerable: true,
      configurable: true,
    }
  });

9.8.2 オブジェクトリテラルによって作成された独自のプロパティ

const obj = { prop: 'yes' };

assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    prop: {
      value: 'yes',
      writable: true,
      enumerable: true,
      configurable: true
    }
  });

9.8.3 配列の独自のプロパティ.length

配列の独自のプロパティ.lengthは列挙不可能であるため、Object.assign()、スプレッド、および同様の操作によってコピーされません。また、構成不可能です。

> Object.getOwnPropertyDescriptor([], 'length')
{ value: 0, writable: true, enumerable: false, configurable: false }
> Object.getOwnPropertyDescriptor('abc', 'length')
{ value: 3, writable: false, enumerable: false, configurable: false }

.lengthは、他の独自のプロパティ(特に、インデックスプロパティ)によって影響を受け、(そして影響を与える)特別なデータプロパティです。

9.8.4 組み込みクラスのプロトタイププロパティ

assert.deepEqual(
  Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
  {
    value: Array.prototype.map,
    writable: true,
    enumerable: false,
    configurable: true
  });

9.8.5 ユーザー定義クラスのプロトタイププロパティとインスタンスプロパティ

class DataContainer {
  accessCount = 0;
  constructor(data) {
    this.data = data;
  }
  getData() {
    this.accessCount++;
    return this.data;
  }
}
assert.deepEqual(
  Object.getOwnPropertyDescriptors(DataContainer.prototype),
  {
    constructor: {
      value: DataContainer,
      writable: true,
      enumerable: false,
      configurable: true,
    },
    getData: {
      value: DataContainer.prototype.getData,
      writable: true,
      enumerable: false,
      configurable: true,
    }
  });

DataContainerのインスタンスのすべての独自のプロパティは、書き込み可能、列挙可能、および構成可能であることに注意してください。

const dc = new DataContainer('abc')
assert.deepEqual(
  Object.getOwnPropertyDescriptors(dc),
  {
    accessCount: {
      value: 0,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    data: {
      value: 'abc',
      writable: true,
      enumerable: true,
      configurable: true,
    }
  });

9.9 API: プロパティディスクリプタ

次のツールメソッドは、プロパティディスクリプタを使用します。

9.10 参考文献

次の3つの章では、プロパティ属性の詳細について説明します。