for-in
ループからプロパティを隠すJSON.stringify()
から自身のプロパティを隠す列挙可能性はオブジェクトプロパティの属性です。この章では、その使用方法と、Object.keys()
や Object.assign()
などの操作にどのように影響するかを詳しく見ていきます。
必要な知識:プロパティ属性
この章では、プロパティ属性に精通している必要があります。そうでない場合は、§9「プロパティ属性:入門」を確認してください。
さまざまな操作が列挙可能性によってどのように影響を受けるかを示すために、プロトタイプが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,
},
});
操作 | 文字列キー | シンボルキー | 継承 | |
---|---|---|---|---|
Object.keys() |
ES5 | ✔ |
✘ |
✘ |
Object.values() |
ES2017 | ✔ |
✘ |
✘ |
Object.entries() |
ES2017 | ✔ |
✘ |
✘ |
スプレッド{...x} |
ES2018 | ✔ |
✔ |
✘ |
Object.assign() |
ES6 | ✔ |
✔ |
✘ |
JSON.stringify() |
ES5 | ✔ |
✘ |
✘ |
for-in |
ES1 | ✔ |
✘ |
✔ |
次の操作(tbl. 2に要約)では、列挙可能なプロパティのみを考慮します
Object.keys()
[ES5] は、列挙可能な自身の文字列キー付きプロパティのキーを返します。
Object.values()
[ES2017] は、列挙可能な自身の文字列キー付きプロパティの値を返します。
Object.entries()
[ES2017] は、列挙可能な自身の文字列キー付きプロパティのキーと値のペアを返します。(Object.fromEntries()
はキーとしてシンボルを受け入れますが、列挙可能なプロパティのみを作成することに注意してください。)
オブジェクトリテラルへのスプレッド[ES2018]は、自身の列挙可能なプロパティ(文字列キーまたはシンボルキー付き)のみを考慮します。
Object.assign()
[ES6] は、列挙可能な自身のプロパティ(文字列キーまたはシンボルキーのいずれか)のみをコピーします。
JSON.stringify()
[ES5] は、文字列キーを持つ列挙可能な自身のプロパティのみを文字列化します。
for-in
ループ [ES1] は、自身の継承された列挙可能な文字列キー付きプロパティのキーを走査します。
for-in
は、継承されたプロパティに対して列挙可能性が重要になる唯一の組み込み操作です。他のすべての操作は、自身のプロパティでのみ機能します。
操作 | 文字列キー | シンボルキー | 継承 | |
---|---|---|---|---|
Object.getOwnPropertyNames() |
ES5 | ✔ |
✘ |
✘ |
Object.getOwnPropertySymbols() |
ES6 | ✘ |
✔ |
✘ |
Reflect.ownKeys() |
ES6 | ✔ |
✔ |
✘ |
Object.getOwnPropertyDescriptors() |
ES2017 | ✔ |
✔ |
✘ |
次の操作(tbl. 3に要約)では、列挙可能および列挙不可能なプロパティの両方を考慮します
Object.getOwnPropertyNames()
[ES5] は、自身のすべての文字列キー付きプロパティのキーをリストします。
Object.getOwnPropertySymbols()
[ES6] は、自身のすべてのシンボルキー付きプロパティのキーをリストします。
Reflect.ownKeys()
[ES6] は、自身のすべてのプロパティのキーをリストします。
Object.getOwnPropertyDescriptors()
[ES2017] は、自身のすべてのプロパティのプロパティ記述子をリストします。
> Object.getOwnPropertyDescriptors(obj)
{
objEnumStringKey: {
value: 'objEnumStringKeyValue',
writable: false,
enumerable: true,
configurable: false
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
writable: false,
enumerable: false,
configurable: false
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
writable: false,
enumerable: true,
configurable: false
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
writable: false,
enumerable: false,
configurable: false
}
}
内省により、プログラムは実行時に値の構造を調べることができます。これはメタプログラミングです。通常のプログラミングはプログラムを作成することです。メタプログラミングは、プログラムを調べたり変更したりすることです。
JavaScriptでは、一般的な内省的な操作には短い名前が付けられ、めったに使用されない操作には長い名前が付けられます。列挙不可能なプロパティを無視することは標準であるため、それを行う操作には短い名前が付けられ、それを行わない操作には長い名前が付けられます。
Object.keys()
は、列挙不可能なプロパティを無視します。Object.getOwnPropertyNames()
は、自身のすべてのプロパティの文字列キーをリストします。ただし、Reflect
メソッド(Reflect.ownKeys()
など)は、Reflect
がプロキシに関連するより「メタ」な操作を提供するため、このルールから逸脱します。
さらに、次の区別がなされます(シンボルを導入したES6以降)。
したがって、Object.keys()
のより適切な名前は、Object.names()
になります。
このセクションでは、Object.getOwnPropertyDescriptor()
を次のように省略します
ほとんどのデータプロパティは、次の属性で作成されます。
これには以下が含まれます。
Object.fromEntries()
最も重要な列挙不可能なプロパティは次のとおりです。
組み込みクラスのプロトタイププロパティ
ユーザー定義クラスを介して作成されたプロトタイププロパティ
配列のプロパティ.length
文字列のプロパティ.length
(プリミティブ値のすべてのプロパティは読み取り専用であることに注意してください)
次に、列挙可能性のユースケースを見ていきます。これにより、一部のプロパティが列挙可能であり、他のプロパティが列挙可能でない理由がわかります。
列挙可能性は一貫性のない機能です。ユースケースはありますが、常に何らかの注意点があります。このセクションでは、ユースケースと注意点を見ていきます。
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;
}
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()
などの継承されたメソッドを、それらがすべて列挙不可能であるために認識しません
ユーザー定義クラスでは、すべての継承されたプロパティも列挙不可能であるため、無視されます
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()
などを組み合わせる方が良いでしょう。
for-in
を使用する際の注意点自身のプロパティ.length
は、配列と文字列では列挙不可能であるため、for-in
によって無視されます
ただし、配列のインデックスを反復処理するために 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
は、インデックスプロパティと非インデックスプロパティの両方を考慮するため、配列のインデックスを反復処理するために使用しないでください
配列のキーに関心がある場合は、配列メソッド .keys()
を使用してください
配列の要素を反復処理する場合は、for-of
ループを使用します。これには、他の反復可能なデータ構造でも機能するという利点もあります。
プロパティを列挙不可能にすることで、一部のコピー操作からプロパティを隠すことができます。最初に、より最新のコピー操作に進む前に、2つの歴史的なコピー操作を調べてみましょう。
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});
$.extend()
jQueryの $.extend(target, source1, source2, ···)
は、Object.extend()
と同様に機能します
source1
のすべての列挙可能な自身と継承されたプロパティを target
の自身のプロパティにコピーします。source2
で同じことを行います。コピーを列挙可能性に基づくことには、いくつかの欠点があります
列挙可能性は継承されたプロパティを隠すのに役立ちますが、通常は自身のプロパティを自身のプロパティにのみコピーしたいため、主にこの方法で使用されます。同じ効果は、継承されたプロパティを無視することでより適切に実現できます。
コピーするプロパティは、多くの場合、目の前のタスクによって異なります。すべてのユースケースに対応する単一のフラグを持つことはほとんど意味がありません。より良い選択肢は、プロパティを無視するときを伝える述語(ブール値を返すコールバック)を使用してコピー操作を提供することです。
列挙可能性は、コピー時に配列の自身のプロパティ .length
を便利に隠します。ただし、それは非常にまれな例外的なケースです。兄弟プロパティに影響を与え、それらによって影響を受ける魔法のプロパティです。このような魔法を自分で実装する場合は、(継承された)ゲッターまたはセッターを使用し、(自身の)データプロパティは使用しません。
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
よりもクリーンです。
非列挙可能性が役立つケースはほとんどありません。まれな例としては、ライブラリ fs-extra
が最近抱えていた問題があります。
組み込みの Node.js モジュール fs
には、fs
API の Promise ベースのバージョンを含むオブジェクトを持つ .promises
プロパティがあります。問題当時、.promise
を読み取ると、次の警告がコンソールにログ出力されていました。
ExperimentalWarning: The fs.promises API is experimental
fs-extra
は、独自の機能を提供するだけでなく、fs
にあるすべてのものを再エクスポートします。CommonJS モジュールの場合、これは fs
のすべてのプロパティを fs-extra
の module.exports
にコピーすることを意味します(Object.assign()
経由)。そして、fs-extra
がそれを行ったとき、警告がトリガーされました。これは、fs-extra
がロードされるたびに発生するため、混乱を招きました。
迅速な修正として、プロパティ fs.promises
を非列挙可能にしました。その後、fs-extra
はそれを無視しました。
プロパティを非列挙可能にすると、Object.keys()
、for-in
ループなどでは見えなくなります。これらのメカニズムに関しては、プロパティはプライベートです。
ただし、このアプローチにはいくつかの問題があります。
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()
が列挙可能性よりもクリーンだと思います。また、ストレージ形式がどのようにあるべきかについても、より自由度が高まります。
非列挙可能性のほとんどすべての応用は、現在では他のより良い解決策がある回避策であることがわかりました。
私たち自身のコードでは、通常、列挙可能性が存在しないふりをすることができます。
つまり、私たちは自動的にベストプラクティスに従います。