Object.create():記述子によるオブジェクトの作成Object.getOwnPropertyDescriptors() のユースケース.lengthこの章では、ECMAScript 仕様が JavaScript オブジェクトをどのように見ているかについて詳しく見ていきます。特に、プロパティは仕様上アトミックではなく、複数の属性(レコードのフィールドと考えてください)で構成されています。データプロパティの値でさえ、属性に格納されます!
ECMAScript 仕様では、オブジェクトは以下で構成されます。
仕様は、内部スロットを次のように説明しています。箇条書きを追加し、一部を強調しました。
undefined です。[[ ]] で囲まれた名前を使用して識別されます。内部スロットには2種類あります。
通常のオブジェクトには、次のデータスロットがあります。
.[[Prototype]]:null | object
Object.getPrototypeOf() および Object.setPrototypeOf() を介して間接的にアクセスできます。.[[Extensible]]:ブール値
Object.preventExtensions() を使用して false に設定できます。.[[PrivateFieldValues]]:EntryList
プロパティのキーは次のいずれかです。
プロパティには2種類あり、それらは属性によって特徴付けられます。
value は任意の JavaScript 値を保持します。get に、後者は属性 set に格納されます。さらに、両方の種類のプロパティが持つ属性があります。次の表に、すべての属性とそのデフォルト値を示します。
| プロパティの種類 | 属性の名前と型 | デフォルト値 |
|---|---|---|
| データプロパティ | value:任意 |
undefined |
writable:ブール値 |
false |
|
| アクセサープロパティ | get:(this: any) => any |
undefined |
set:(this: any, v: any) => void |
undefined |
|
| すべてのプロパティ | configurable:ブール値 |
false |
enumerable:ブール値 |
false |
すでに属性 value、get、および set については説明しました。その他の属性は次のように機能します。
writable は、データプロパティの値を変更できるかどうかを決定します。configurable は、プロパティの属性を変更できるかどうかを決定します。これが false の場合、次のようになります。value 以外の属性を変更することはできません。writable を true から false に変更できます。この異常の背後にある根拠は、歴史的なものです。配列のプロパティ .length は常に書き込み可能であり、設定不可でした。writable 属性の変更を許可することで、配列をフリーズできます。enumerable は、一部の操作(Object.keys() など)に影響します。false の場合、これらの操作はプロパティを無視します。ほとんどのプロパティは列挙可能(代入またはオブジェクトリテラルで作成されたものなど)であるため、実際にこの属性に気づくことはめったにありません。これがどのように機能するかについてまだ興味がある場合は、§12「プロパティの列挙可能性」を参照してください。継承されたプロパティが書き込み不可の場合、代入を使用して同じキーを持つ自身のプロパティを作成することはできません。
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「継承された読み取り専用プロパティは、代入による自身のプロパティの作成を妨げる」を参照してください。
プロパティ記述子は、プロパティの属性を 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「記述子プロパティの省略」では、それらを省略した場合に何が起こるかを説明しています。
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() が確実に機能します。
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() が確実に機能します。
プロパティ記述子 propDesc を介してキー k を持つプロパティを定義する場合、何が起こるかは次のようになります。
k を持つプロパティがない場合は、propDesc で指定された属性を持つ新しい自身のプロパティが作成されます。k を持つプロパティがある場合は、定義によってプロパティの属性が propDesc と一致するように変更されます。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',
});代入と同じプロパティ属性を使用しました。
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})`;
},
});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!Object.getOwnPropertyDescriptors() のユースケースObject.getOwnPropertyDescriptors() は、Object.defineProperties() または Object.create() と組み合わせることで、2つのユースケースに役立ちます。
ES6以降、JavaScriptにはプロパティをコピーするためのツールメソッド Object.assign() がすでにあります。ただし、このメソッドは、キーが key のプロパティをコピーするために、単純な get および set 操作を使用します。
つまり、プロパティの忠実なコピーを作成するのは、次の場合のみです。
writable が true であり、その属性 enumerable が true である(代入によってプロパティが作成されるため)。次の例は、この制限事項を示しています。オブジェクト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,
});superを使用するメソッドのコピーsuperを使用するメソッドは、そのホームオブジェクト(それが格納されているオブジェクト)と密接に結びついています。現在、このようなメソッドを別のオブジェクトにコピーまたは移動する方法はありません。
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「オブジェクトと配列のコピー」を参照してください。
ディスクリプタのすべてのプロパティはオプションです。プロパティを省略した場合に何が起こるかは、操作によって異なります。
ディスクリプタを介して新しいプロパティを作成する場合、属性を省略すると、それらのデフォルト値が使用されます。
const car = {};
Object.defineProperty(
car, 'color', {
value: 'red',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'red',
writable: false,
enumerable: false,
configurable: false,
});代わりに、既存のプロパティを変更する場合、ディスクリプタプロパティを省略すると、対応する属性は変更されません。
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,
});プロパティ属性の一般的なルールは(いくつかの例外を除いて)、次のとおりです。
プロトタイプチェーンの先頭にあるオブジェクトのプロパティは、通常、書き込み可能、列挙可能、および構成可能です。
列挙可能性に関する章で説明されているように、ほとんどの継承されたプロパティは、for-inループなどのレガシー構文から隠すために、列挙不可能です。継承されたプロパティは、通常、書き込み可能で構成可能です。
const obj = {};
obj.prop = 3;
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
}
});const obj = { prop: 'yes' };
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 'yes',
writable: true,
enumerable: true,
configurable: true
}
});.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は、他の独自のプロパティ(特に、インデックスプロパティ)によって影響を受け、(そして影響を与える)特別なデータプロパティです。
assert.deepEqual(
Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
{
value: Array.prototype.map,
writable: true,
enumerable: false,
configurable: true
});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,
}
});次のツールメソッドは、プロパティディスクリプタを使用します。
Object.defineProperty(obj: object, key: string|symbol, propDesc: PropertyDescriptor): object [ES5]
obj上のキーがkeyであり、属性がpropDescを介して指定されたプロパティを作成または変更します。変更されたオブジェクトを返します。
Object.defineProperties(obj: object, properties: {[k: string|symbol]: PropertyDescriptor}): object [ES5]
Object.defineProperty()のバッチバージョンです。オブジェクトpropertiesの各プロパティpは、objに追加される1つのプロパティを指定します。pのキーはプロパティのキーを指定し、pの値はプロパティの属性を指定するディスクリプタです。
Object.create(proto: null|object, properties?: {[k: string|symbol]: PropertyDescriptor}): object [ES5]
最初に、プロトタイプがprotoであるオブジェクトを作成します。次に、オプションのパラメータpropertiesが指定されている場合は、Object.defineProperties()と同じ方法でプロパティを追加します。最後に、結果を返します。たとえば、次のコードスニペットは、前のスニペットと同じ結果を生成します。
Object.getOwnPropertyDescriptor(obj: object, key: string|symbol): undefined|PropertyDescriptor [ES5]
キーがkeyであるobjの独自の(継承されていない)プロパティのディスクリプタを返します。そのようなプロパティがない場合は、undefinedが返されます。
Object.getOwnPropertyDescriptors(obj: object): {[k: string|symbol]: PropertyDescriptor} [ES2017]
objの各プロパティキー'k'がobj.kのプロパティディスクリプタにマップされたオブジェクトを返します。結果は、Object.defineProperties()およびObject.create()の入力として使用できます。
const propertyKey = Symbol('propertyKey');
const obj = {
[propertyKey]: 'abc',
get count() { return 123 },
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
[propertyKey]: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true
},
count: {
get: desc(obj, 'count').get, // (A)
set: undefined,
enumerable: true,
configurable: true
}
});A行でdesc()を使用することは、.deepEqual()が機能するようにするための回避策です。
次の3つの章では、プロパティ属性の詳細について説明します。