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

12 プロパティの列挙可能性



列挙可能性はオブジェクトプロパティの属性です。この章では、その使用方法と、Object.keys()Object.assign() などの操作にどのように影響するかを詳しく見ていきます。

  必要な知識:プロパティ属性

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

12.1 列挙可能性がプロパティを反復処理する構造にどのように影響するか

さまざまな操作が列挙可能性によってどのように影響を受けるかを示すために、プロトタイプがprotoである次のオブジェクトobjを使用します。

const protoEnumSymbolKey = Symbol('protoEnumSymbolKey');
const protoNonEnumSymbolKey = Symbol('protoNonEnumSymbolKey');
const proto = Object.defineProperties({}, {
  protoEnumStringKey: {
    value: 'protoEnumStringKeyValue',
    enumerable: true,
  },
  [protoEnumSymbolKey]: {
    value: 'protoEnumSymbolKeyValue',
    enumerable: true,
  },
  protoNonEnumStringKey: {
    value: 'protoNonEnumStringKeyValue',
    enumerable: false,
  },
  [protoNonEnumSymbolKey]: {
    value: 'protoNonEnumSymbolKeyValue',
    enumerable: false,
  },
});

const objEnumSymbolKey = Symbol('objEnumSymbolKey');
const objNonEnumSymbolKey = Symbol('objNonEnumSymbolKey');
const obj = Object.create(proto, {
  objEnumStringKey: {
    value: 'objEnumStringKeyValue',
    enumerable: true,
  },
  [objEnumSymbolKey]: {
    value: 'objEnumSymbolKeyValue',
    enumerable: true,
  },
  objNonEnumStringKey: {
    value: 'objNonEnumStringKeyValue',
    enumerable: false,
  },
  [objNonEnumSymbolKey]: {
    value: 'objNonEnumSymbolKeyValue',
    enumerable: false,
  },
});

12.1.1 列挙可能なプロパティのみを考慮する操作

表2:列挙不可能なプロパティを無視する操作。
操作 文字列キー シンボルキー 継承
Object.keys() ES5
Object.values() ES2017
Object.entries() ES2017
スプレッド{...x} ES2018
Object.assign() ES6
JSON.stringify() ES5
for-in ES1

次の操作(tbl. 2に要約)では、列挙可能なプロパティのみを考慮します

for-in は、継承されたプロパティに対して列挙可能性が重要になる唯一の組み込み操作です。他のすべての操作は、自身のプロパティでのみ機能します。

12.1.2 列挙可能および列挙不可能なプロパティの両方を考慮する操作

表3:列挙可能および列挙不可能なプロパティの両方を考慮する操作。
操作 文字列キー シンボルキー 継承
Object.getOwnPropertyNames() ES5
Object.getOwnPropertySymbols() ES6
Reflect.ownKeys() ES6
Object.getOwnPropertyDescriptors() ES2017

次の操作(tbl. 3に要約)では、列挙可能および列挙不可能なプロパティの両方を考慮します

12.1.3 内省的な操作の命名規則

内省により、プログラムは実行時に値の構造を調べることができます。これはメタプログラミングです。通常のプログラミングはプログラムを作成することです。メタプログラミングは、プログラムを調べたり変更したりすることです。

JavaScriptでは、一般的な内省的な操作には短い名前が付けられ、めったに使用されない操作には長い名前が付けられます。列挙不可能なプロパティを無視することは標準であるため、それを行う操作には短い名前が付けられ、それを行わない操作には長い名前が付けられます。

ただし、Reflect メソッド(Reflect.ownKeys()など)は、Reflectがプロキシに関連するより「メタ」な操作を提供するため、このルールから逸脱します。

さらに、次の区別がなされます(シンボルを導入したES6以降)。

したがって、Object.keys() のより適切な名前は、Object.names() になります。

12.2 事前定義されたプロパティと作成されたプロパティの列挙可能性

このセクションでは、Object.getOwnPropertyDescriptor() を次のように省略します

const desc = Object.getOwnPropertyDescriptor.bind(Object);

ほとんどのデータプロパティは、次の属性で作成されます。

{
  writable: true,
  enumerable: false,
  configurable: true,
}

これには以下が含まれます。

最も重要な列挙不可能なプロパティは次のとおりです。

次に、列挙可能性のユースケースを見ていきます。これにより、一部のプロパティが列挙可能であり、他のプロパティが列挙可能でない理由がわかります。

12.3 列挙可能性のユースケース

列挙可能性は一貫性のない機能です。ユースケースはありますが、常に何らかの注意点があります。このセクションでは、ユースケースと注意点を見ていきます。

12.3.1 ユースケース: for-in ループからプロパティを隠す

for-in ループは、オブジェクトのすべての列挙可能な文字列キー付きプロパティ(自身と継承されたもの)を走査します。したがって、属性enumerableは、走査すべきでないプロパティを隠すために使用されます。これがECMAScript 1で列挙可能性を導入した理由でした。

一般的に、for-in を避けるのが最善です。次の2つのサブセクションで理由を説明します。次の関数は、for-in がどのように機能するかを示すのに役立ちます。

function listPropertiesViaForIn(obj) {
  const result = [];
  for (const key in obj) {
    result.push(key);
  }
  return result;
}
12.3.1.1 オブジェクトにfor-inを使用する際の注意点

for-in は、継承されたものも含めて、すべてのプロパティを反復処理します

const proto = {enumerableProtoProp: 1};
const obj = {
  __proto__: proto,
  enumerableObjProp: 2,
};
assert.deepEqual(
  listPropertiesViaForIn(obj),
  ['enumerableObjProp', 'enumerableProtoProp']);

通常のプレーンオブジェクトでは、for-in は、Object.prototype.toString() などの継承されたメソッドを、それらがすべて列挙不可能であるために認識しません

const obj = {};
assert.deepEqual(
  listPropertiesViaForIn(obj),
  []);

ユーザー定義クラスでは、すべての継承されたプロパティも列挙不可能であるため、無視されます

class Person {
  constructor(first, last) {
    this.first = first;
    this.last = last;
  }
  getName() {
    return this.first + ' ' + this.last;
  }
}
const jane = new Person('Jane', 'Doe');
assert.deepEqual(
  listPropertiesViaForIn(jane),
  ['first', 'last']);

結論:オブジェクトでは、for-in は継承されたプロパティを考慮しますが、通常はそれらを無視したいと思います。それから、for-of ループと Object.keys()Object.entries() などを組み合わせる方が良いでしょう。

12.3.1.2 配列にfor-inを使用する際の注意点

自身のプロパティ.length は、配列と文字列では列挙不可能であるため、for-in によって無視されます

> listPropertiesViaForIn(['a', 'b'])
[ '0', '1' ]
> listPropertiesViaForIn('ab')
[ '0', '1' ]

ただし、配列のインデックスを反復処理するために for-in を使用することは一般的に安全ではありません。これは、インデックスではない継承されたプロパティと自身のプロパティの両方を考慮するためです。次の例は、配列に自身の非インデックスプロパティがある場合に何が起こるかを示します

const arr1 = ['a', 'b'];
assert.deepEqual(
  listPropertiesViaForIn(arr1),
  ['0', '1']);

const arr2 = ['a', 'b'];
arr2.nonIndexProp = 'yes';
assert.deepEqual(
  listPropertiesViaForIn(arr2),
  ['0', '1', 'nonIndexProp']);

結論:for-in は、インデックスプロパティと非インデックスプロパティの両方を考慮するため、配列のインデックスを反復処理するために使用しないでください

12.3.2 ユースケース: コピーしないプロパティとしてマークする

プロパティを列挙不可能にすることで、一部のコピー操作からプロパティを隠すことができます。最初に、より最新のコピー操作に進む前に、2つの歴史的なコピー操作を調べてみましょう。

12.3.2.1 歴史的なコピー操作: Prototypeの Object.extend()

Prototypeは、2005年2月にSam Stephensonによって、Ruby on RailsでのAjaxサポートの基盤の一部として作成されたJavaScriptフレームワークです。

Prototypeの Object.extend(destination, source) は、sourceのすべての列挙可能な自身と継承されたプロパティをdestinationの自身のプロパティにコピーします。次のように実装されています

function extend(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
}

オブジェクトで Object.extend() を使用すると、継承されたプロパティを自身のプロパティにコピーし、列挙不可能なプロパティを無視することがわかります(シンボルキー付きプロパティも無視します)。これはすべて、for-in がどのように機能するかによるものです。

const proto = Object.defineProperties({}, {
  enumProtoProp: {
    value: 1,
    enumerable: true,
  },
  nonEnumProtoProp: {
    value: 2,
    enumerable: false,
  },
});
const obj = Object.create(proto, {
  enumObjProp: {
    value: 3,
    enumerable: true,
  },
  nonEnumObjProp: {
    value: 4,
    enumerable: false,
  },
});

assert.deepEqual(
  extend({}, obj),
  {enumObjProp: 3, enumProtoProp: 1});
12.3.2.2 歴史的なコピー操作: jQueryの $.extend()

jQueryの $.extend(target, source1, source2, ···) は、Object.extend() と同様に機能します

12.3.2.3 列挙可能性駆動のコピーの欠点

コピーを列挙可能性に基づくことには、いくつかの欠点があります

12.3.2.4 Object.assign() [ES5]

ES6では、Object.assign(target, source_1, source_2, ···) を使用して、ソースをターゲットにマージできます。ソースのすべての自身の列挙可能なプロパティ(文字列キーまたはシンボルキー付き)が考慮されます。Object.assign() は、「get」操作を使用してソースから値を読み取り、「set」操作を使用してターゲットに値を書き込みます。

列挙可能性に関して、Object.assign()Object.extend() および $.extend() の伝統を引き継ぎます。Yehuda Katzを引用すると

Object.assign は、すでに流通しているすべての extend() API の先例に従います。それらのケースで列挙可能なメソッドをコピーしない前例が、Object.assign がこの動作をするのに十分な理由であると考えました。

言い換えれば、Object.assign()$.extend() (および類似のもの)からのアップグレードパスを念頭に置いて作成されました。そのアプローチは、継承されたプロパティを無視するため、$.extend よりもクリーンです。

12.3.2.5 コピー時に非列挙可能性が役立つまれな例

非列挙可能性が役立つケースはほとんどありません。まれな例としては、ライブラリ fs-extra が最近抱えていた問題があります。

12.3.3 プロパティをプライベートとしてマークする

プロパティを非列挙可能にすると、Object.keys()for-in ループなどでは見えなくなります。これらのメカニズムに関しては、プロパティはプライベートです。

ただし、このアプローチにはいくつかの問題があります。

12.3.4 JSON.stringify() から自身のプロパティを隠す

JSON.stringify() は、非列挙可能なプロパティを出力に含めません。したがって、列挙可能性を使用して、JSON にエクスポートする必要のある自身のプロパティを決定できます。このユースケースは、前の「プロパティをプライベートとしてマークする」ユースケースに似ています。しかし、エクスポートに関するものであり、考慮事項が若干異なるため、これも異なります。たとえば、オブジェクトを JSON から完全に再構築できますか?

列挙可能性の代わりに、オブジェクトは .toJSON() メソッドを実装でき、JSON.stringify() はオブジェクト自体ではなく、そのメソッドが返すものを文字列化します。次の例は、その仕組みを示しています。

class Point {
  static fromJSON(json) {
    return new Point(json[0], json[1]);
  }
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toJSON() {
    return [this.x, this.y];
  }
}
assert.equal(
  JSON.stringify(new Point(8, -3)),
  '[8,-3]'
);

私は toJSON() が列挙可能性よりもクリーンだと思います。また、ストレージ形式がどのようにあるべきかについても、より自由度が高まります。

12.4 結論

非列挙可能性のほとんどすべての応用は、現在では他のより良い解決策がある回避策であることがわかりました。

私たち自身のコードでは、通常、列挙可能性が存在しないふりをすることができます。

つまり、私たちは自動的にベストプラクティスに従います。