0
1

5-②.JavaScript のオブジェクト(属性記述オブジェクト)

Last updated at Posted at 2024-07-20

概要

JavaScript は、オブジェクトの属性を記述し、その動作を制御するための内部データ構造を提供しています。たとえば、属性が書き込み可能かどうか、列挙可能かどうかなどです。この内部データ構造は「属性記述オブジェクト」(attributes object)と呼ばれます。各属性には対応する属性記述オブジェクトがあり、その属性に関するメタ情報を保持します。

以下は属性記述オブジェクトの例です。

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

属性記述オブジェクトは6つのメタ属性を提供します。

(1)value

valueはその属性の値で、デフォルトはundefinedです。

(2)writable

writableはブール値で、属性の値(value)が変更可能かどうかを示します(つまり書き込み可能かどうか)。デフォルトはtrueです。

(3)enumerable

enumerableはブール値で、その属性が列挙可能かどうかを示します。デフォルトはtrueです。falseに設定すると、for...inループやObject.keys()のような操作がその属性をスキップします。

(4)configurable

configurableはブール値で、属性の構成可能性を示します。デフォルトはtrueです。falseに設定すると、属性記述オブジェクトの変更や属性の削除ができなくなります(value属性を除く)。つまり、configurable属性は属性記述オブジェクトの書き込み可能性を制御します。

(5)get

getは関数で、その属性の取得関数(getter)を示します。デフォルトはundefinedです。

(6)set

setは関数で、その属性の設定関数(setter)を示します。デフォルトはundefinedです。

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()メソッドは属性記述オブジェクトを取得します。最初の引数は対象オブジェクトで、2番目の引数は対象オブジェクトの属性名に対応する文字列です。

var obj = { p: 'a' };

Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

上記のコードでは、Object.getOwnPropertyDescriptor()メソッドはobj.pの属性記述オブジェクトを取得します。

注意点として、Object.getOwnPropertyDescriptor()メソッドはオブジェクト自身の属性にのみ使用でき、継承された属性には使用できません。


var obj = { p: 'a' };

Object.getOwnPropertyDescriptor(obj, 'toString')
// undefined

上記のコードでは、toStringはobjオブジェクトの継承された属性であり、Object.getOwnPropertyDescriptor()では取得できません。

Object.getOwnPropertyNames()

Object.getOwnPropertyNamesメソッドは、引数オブジェクト自身のすべての属性名を含む配列を返します。これには、その属性が列挙可能かどうかに関わらず、すべての属性が含まれます。

var obj = Object.defineProperties({}, {
  p1: { value: 1, enumerable: true },
  p2: { value: 2, enumerable: false }
});

Object.getOwnPropertyNames(obj)
// ["p1", "p2"]

上記のコードでは、obj.p1は列挙可能で、obj.p2は列挙不可能です。しかし、Object.getOwnPropertyNamesは両方の属性を返します。

これは、Object.keysの動作とは異なります。Object.keysはオブジェクト自身の列挙可能な属性名のみを返します。

Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]

Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
//  'valueOf',
//  'constructor',
//  'toLocaleString',
//  'isPrototypeOf',
//  'propertyIsEnumerable',
//  'toString']

上記のコードでは、配列自身のlength属性は列挙不可能であるため、Object.keysはその属性を返しません。2つ目の例のObject.prototypeもオブジェクトであり、すべてのインスタンスオブジェクトがそれを継承しますが、その自身の属性はすべて列挙不可能です。

Object.defineProperty(),Object.defineProperties()

Object.defineProperty()メソッドは属性記述オブジェクトを通じて属性を定義または変更し、変更されたオブジェクトを返します。使用方法は次のとおりです。

Object.defineProperty(object, propertyName, attributesObject)

Object.definePropertyメソッドは3つの引数を取ります。

object:属性が存在するオブジェクト
propertyName:属性名を示す文字列
attributesObject:属性記述オブジェクト
例えば、obj.pを定義するには次のように書きます。

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false
});

obj.p // 123

obj.p = 246;
obj.p // 123

上記のコードでは、Object.defineProperty()メソッドを使ってobj.p属性を定義しました。属性記述オブジェクトのwritable属性がfalseであるため、obj.p属性は書き込み不可です。ここでのObject.definePropertyメソッドの最初の引数は{}(新しく作成された空のオブジェクト)であり、p属性はこの空のオブジェクト上に直接定義され、返されるオブジェクトはこのオブジェクトです。これはObject.defineProperty()の一般的な使い方です。

すでに属性が存在する場合、Object.defineProperty()メソッドはその属性の属性記述オブジェクトを更新することに相当します。

一度に複数の属性を定義または変更する場合、Object.defineProperties()メソッドを使用できます。

var obj = Object.defineProperties({}, {
  p1: { value: 123, enumerable: true },
  p2: { value: 'abc', enumerable: true },
  p3: { get: function () { return this.p1 + this.p2 },
    enumerable:true,
    configurable:true
  }
});

obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"

上記のコードでは、Object.defineProperties()を使ってobjオブジェクトの3つの属性を同時に定義しました。その中で、p3属性は取得関数getを定義しており、毎回その属性を読み取るときにこの取得関数が呼び出されます。

注意点として、取得関数getまたは設定関数setを定義した場合、writable属性をtrueに設定することはできず、またはvalue属性を同時に定義することもできません。そうでないとエラーが発生します。

var obj = {};

Object.defineProperty(obj, 'p', {
  value: 123,
  get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value

Object.defineProperty(obj, 'p', {
  writable: true,
  get: function() { return 456; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute

上記のコードでは、get属性とvalue属性を同時に定義し、さらにwritable属性をtrueに設定するとエラーが発生します。

Object.defineProperty()とObject.defineProperties()の引数内の属性記述オブジェクトのwritable、configurable、enumerableの3つの属性のデフォルト値はすべてfalseです。

var obj = {};
Object.defineProperty(obj, 'foo', {});
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
//   value: undefined,
//   writable: false,
//   enumerable: false,
//   configurable: false
// }

上記のコードでは、obj.fooを定義する際に空の属性記述オブジェクトを使用したため、各メタ属性のデフォルト値を確認できます。

Object.prototype.propertyIsEnumerable()

インスタンスオブジェクトのpropertyIsEnumerable()メソッドは、特定の属性が列挙可能かどうかを判定するために使用されるブール値を返します。このメソッドは、オブジェクト自身の属性に対してのみ使用でき、継承された属性に対しては常にfalseを返します。

var obj = {};
obj.p = 123;

obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false

上記のコードでは、obj.pは列挙可能ですが、obj.toStringは継承された属性です。

メタ属性

属性記述オブジェクトの各属性は「メタ属性」と呼ばれます。なぜなら、それらは属性を制御する属性と見なせるからです。

value
value属性は、対象属性の値です。

var obj = {};
obj.p = 123;

Object.getOwnPropertyDescriptor(obj, 'p').value
// 123

Object.defineProperty(obj, 'p', { value: 246 });
obj.p // 246

上記のコードは、value属性を使ってobj.pを読み取ったり、書き換えたりする例です。

writable

writable属性はブール値で、対象属性の値(value)が変更可能かどうかを決定します。

var obj = {};

Object.defineProperty(obj, 'a', {
  value: 37,
  writable: false
});

obj.a // 37
obj.a = 25;
obj.a // 37

上記のコードでは、obj.aのwritable属性はfalseです。その後、obj.aの値を変更しても、何の効果もありません。

注意点として、通常モードでは、writableがfalseの属性に値を代入してもエラーは発生せず、ただ失敗します。しかし、厳密モードではエラーが発生します。同じ値を代入しようとしてもエラーになります。

'use strict';
var obj = {};

Object.defineProperty(obj, 'a', {
  value: 37,
  writable: false
});

obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property 'a' of object

上記のコードは、厳密モードであり、obj.aに対する代入操作はすべてエラーを発生させます。

もし、プロトタイプオブジェクトのある属性のwritableがfalseである場合、サブオブジェクトはその属性を自分で定義することができません。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var obj = Object.create(proto);

obj.foo = 'b';
obj.foo // 'a'

上記のコードでは、protoはプロトタイプオブジェクトであり、そのfoo属性は書き込み不可です。objオブジェクトはprotoを継承しており、その属性を自分で定義することができません。厳密モードでは、これを行うとエラーが発生します。

しかし、属性記述オブジェクトを上書きすることで、この制限を回避する方法があります。この場合、プロトタイプチェーンは完全に無視されます。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});

var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', {
  value: 'b'
});

obj.foo // "b"

enumerable

enumerable(列挙可能性)はブール値で、対象属性が列挙可能かどうかを示します。

JavaScript の初期バージョンでは、for...inループはin演算子に基づいていました。in演算子は、属性がオブジェクト自身のものであろうと継承されたものであろうと、trueを返します。

var obj = {};
'toString' in obj // true

上記のコードでは、toStringはobjオブジェクト自身の属性ではありませんが、in演算子はtrueを返します。これにより、for...inループはtoString属性も列挙することになります。

これは明らかに合理的ではないため、後に「列挙可能性」という概念が導入されました。列挙可能な属性だけがfor...inループで列挙されるようになり、さらにtoStringのようなインスタンスオブジェクトが継承するネイティブ属性は列挙不可能とされました。これにより、for...inループの有用性が確保されました。

具体的には、属性のenumerableがfalseの場合、以下の3つの操作ではその属性を取得できません。

for..inループ
Object.keysメソッド
JSON.stringifyメソッド
したがって、enumerableは「秘密の」属性を設定するために使用できます。

var obj = {};

Object.defineProperty(obj, 'x', {
  value: 123,
  enumerable: false
});

obj.x // 123

for (var key in obj) {
  console.log(key);
}
// undefined

Object.keys(obj)  // []
JSON.stringify(obj) // "{}"

上記のコードでは、obj.x属性のenumerableがfalseであるため、一般的な列挙操作ではその属性を取得できません。これにより、それは「秘密の」属性のように見えますが、実際には私有属性ではなく、値を直接取得できます。

注意点として、for...inループは継承された属性も含み、Object.keysメソッドは継承された属性を含みません。オブジェクト自身のすべての属性を取得する必要がある場合は、Object.getOwnPropertyNamesメソッドを使用できます。

また、JSON.stringifyメソッドはenumerableがfalseの属性を除外します。これは時々利用できる特性です。オブジェクトのJSON形式出力から特定の属性を除外する必要がある場合、その属性のenumerableをfalseに設定できます。

configurable

configurable(構成可能性)はブール値で、属性記述オブジェクトを変更できるかどうかを決定します。つまり、configurableがfalseの場合、writable、enumerable、configurableを変更することはできません。

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false
});

Object.defineProperty(obj, 'p', {writable: true})
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, 'p', {enumerable: true})
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, 'p', {configurable: true})
// TypeError: Cannot redefine property: p

Object.defineProperty(obj, 'p', {value: 2})
// TypeError: Cannot redefine property: p

上記のコードでは、obj.pのconfigurable属性がfalseです。その後、writable、enumerable、configurableを変更しようとすると、すべてエラーが発生します。

注意点として、writable属性はfalseからtrueに変更する場合のみエラーが発生し、trueからfalseに変更することは許可されています。

var obj = Object.defineProperty({}, 'p', {
  writable: true,
  configurable: false
});

Object.defineProperty(obj, 'p', {writable: false})
 // 変更成功

value属性の場合、writableとconfigurableのどちらかがtrueであれば、valueの変更が許可されます。

var o1 = Object.defineProperty({}, 'p', {
  value: 1,
  writable: true,
  configurable: false
});

Object.defineProperty(o1, 'p', {value: 2})
// 変更成功

var o2 = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  configurable: true
});

Object.defineProperty(o2, 'p', {value: 2})
// 変更成功

また、writableがfalseの場合、対象属性に直接値を代入してもエラーは発生せず、成功しません。

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  configurable: false
});

obj.p = 2;
obj.p // 1

上記のコードでは、obj.pのwritableがfalseであるため、obj.pに直接値を代入しても効果はありません。厳密モードではエラーが発生します。

構成可能性は、対象属性が削除可能かどうかも決定します(delete)。

var obj = Object.defineProperties({}, {
  p1: { value: 1, configurable: true },
  p2: { value: 2, configurable: false }
});

delete obj.p1 // true
delete obj.p2 // false

obj.p1 // undefined
obj.p2 // 2

上記のコードでは、obj.p1のconfigurableはtrueであり、削除可能ですが、obj.p2は削除できません。

アクセサ

属性は直接定義する以外に、アクセサ(accessor)を使用して定義することもできます。設定関数はsetterと呼ばれ、属性記述オブジェクトのset属性を使用します。取得関数はgetterと呼ばれ、属性記述オブジェクトのget属性を使用します。

対象属性にアクセサを定義すると、その属性の読み取りや設定時に対応する関数が実行されます。この機能を利用すると、属性の読み取りや設定動作をカスタマイズする高度な機能を実現できます。

var obj = Object.defineProperty({}, 'p', {
  get: function () {
    return 'getter';
  },
  set: function (value) {
    console.log('setter: ' + value);
  }
});

obj.p // "getter"
obj.p = 123 // "setter: 123"

上記のコードでは、obj.pにgetとset属性を定義しています。obj.pを取得するとgetが呼び出され、設定するとsetが呼び出されます。

JavaScript には、アクセサのもう一つの書き方があります。


// 書き方二
var obj = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  }
};

上記の2つの書き方は、属性pの読み取りと設定動作は同じですが、いくつかの微妙な違いがあります。最初の書き方では、属性pのconfigurableとenumerableはどちらもfalseであり、属性pは列挙不可能です。2つ目の書き方では、属性pのconfigurableとenumerableはどちらもtrueであり、属性pは列挙可能です。実際の開発では、2つ目の書き方がより一般的に使用されます。

注意点として、取得関数getは引数を受け取れず、設定関数setは1つの引数(属性の値)しか受け取れません。

アクセサは、属性の値がオブジェクト内部のデータに依存する場合によく使用されます。


var obj ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw new Error('新しい値は現在の値より大きくなければなりません');
  }
};

obj.next // 5

obj.next = 10;
obj.next // 10

obj.next = 5;
// Uncaught Error: 新しい値は現在の値より大きくなければなりません

上記のコードでは、next属性の設定関数と取得関数は内部属性$nに依存しています。

オブジェクトのコピー

時々、あるオブジェクトのすべての属性を他のオブジェクトにコピーする必要があります。以下の方法で実現できます。

var extend = function (to, from) {
  for (var property in from) {
    to[property] = from[property];
  }

  return to;
}

extend({}, {
  a: 1
})
// {a: 1}

上記の方法には、アクセサ定義された属性に遭遇すると値のみがコピーされるという問題があります。

extend({}, {
  get a() { return 1 }
})
// {a: 1}

この問題を解決するために、Object.definePropertyメソッドを使用して属性をコピーできます。

var extend = function (to, from) {
  for (var property in from) {
    if (!from.hasOwnProperty(property)) continue;
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }

  return to;
}

extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })

上記のコードでは、hasOwnPropertyの行は継承された属性をフィルタリングするために使用されます。そうでないとエラーが発生する可能性があります。なぜなら、Object.getOwnPropertyDescriptorは継承された属性の属性記述オブジェクトを取得できないからです。

オブジェクトの状態制御

オブジェクトの読み取りや書き込みの状態を凍結して変更を防止する必要がある場合があります。JavaScript は3つの凍結方法を提供しています。最も弱いのはObject.preventExtensions、次にObject.seal、最強なのはObject.freezeです。

Object.preventExtensions()
Object.preventExtensionsメソッドは、オブジェクトに新しい属性を追加できないようにします。


var obj = new Object();
Object.preventExtensions(obj);

Object.defineProperty(obj, 'p', {
  value: 'hello'
});
// TypeError: Cannot define property:p, object is not extensible.

obj.p = 1;
obj.p // undefined

上記のコードでは、Object.preventExtensionsを使用すると、objオブジェクトに新しい属性を追加できなくなります。

Object.isExtensible()

Object.isExtensibleメソッドは、オブジェクトがObject.preventExtensionsメソッドを使用したかどうかを確認するために使用されます。つまり、オブジェクトに属性を追加できるかどうかを確認します。

var obj = new Object();

Object.isExtensible(obj) // true
Object.preventExtensions(obj);
Object.isExtensible(obj) // false

上記のコードでは、Object.preventExtensionsメソッドを使用した後、Object.isExtensibleメソッドを使用するとfalseを返し、新しい属性を追加できないことを示します。

Object.seal()

Object.sealメソッドは、オブジェクトに新しい属性を追加できないようにし、既存の属性を削除できないようにします。

var obj = { p: 'hello' };
Object.seal(obj);

delete obj.p;
obj.p // "hello"

obj.x = 'world';
obj.x // undefined

上記のコードでは、Object.sealメソッドを使用すると、objオブジェクトに新しい属性を追加したり、既存の属性を削除したりできなくなります。

Object.sealは、実質的に属性記述オブジェクトのconfigurable属性をfalseに設定することで、属性記述オブジェクトの変更を禁止します。

var obj = {
  p: 'a'
};

// sealメソッドの前
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
//   value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Object.seal(obj);

// sealメソッドの後
Object.getOwnPropertyDescriptor(obj, 'p')
// Object {
//   value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: false
// }

Object.defineProperty(obj, 'p', {
  enumerable: false
})
// TypeError: Cannot redefine property: p

上記のコードでは、Object.sealメソッドを使用すると、属性記述オブジェクトのconfigurable属性がfalseに設定され、その後enumerable属性を変更しようとするとエラーが発生します。

Object.sealは属性の追加や削除を禁止するだけで、特定の属性の値を変更することは影響しません。

var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p // 'b'

上記のコードでは、Object.sealメソッドはp属性のvalueに影響しないため、p属性の値を変更できます。

Object.isSealed()

Object.isSealedメソッドは、オブジェクトがObject.sealメソッドを使用したかどうかを確認するために使用されます。

var obj = { p: 'a' };

Object.seal(obj);
Object.isSealed(obj) // true

この場合、Object.isExtensibleメソッドもfalseを返します。

var obj = { p: 'a' };

Object.seal(obj);
Object.isExtensible(obj) // false


Object.freeze()

Object.freezeメソッドは、オブジェクトに新しい属性を追加できないようにし、既存の属性を削除できないようにし、属性の値も変更できないようにします。この方法により、オブジェクトは事実上定数になります。

var obj = {
  p: 'hello'
};

Object.freeze(obj);

obj.p = 'world';
obj.p // "hello"

obj.t = 'hello';
obj.t // undefined

delete obj.p // false
obj.p // "hello"

上記のコードでは、Object.freeze()を使用すると、objオブジェクトの属性の変更、新しい属性の追加、既存の属性の削除が無効になります。これらの操作はエラーを発生させることなく、ただ失敗します。厳密モードではエラーが発生します。

Object.isFrozen()

Object.isFrozenメソッドは、オブジェクトがObject.freezeメソッドを使用したかどうかを確認するために使用されます。

var obj = {
  p: 'hello'
};

Object.freeze(obj);
Object.isFrozen(obj) // true

Object.freezeメソッドを使用すると、Object.isSealedはtrueを返し、Object.isExtensibleはfalseを返します。

var obj = {
  p: 'hello'
};

Object.freeze(obj);

Object.isSealed(obj) // true
Object.isExtensible(obj) // false

Object.isFrozenのもう一つの用途は、特定のオブジェクトが凍結されていないことを確認してから、その属性に値を代入することです。

var obj = {
  p: 'hello'
};

Object.freeze(obj);

if (!Object.isFrozen(obj)) {
  obj.p = 'world';
}

上記のコードでは、objが凍結されていないことを確認してから属性に値を代入することで、エラーを防ぎます。

制限事項

上記の3つの方法には、オブジェクトの可変性を制限する制限があります。それは、プロトタイプオブジェクトを変更することで属性を追加できることです。

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t
// hello

上記のコードでは、オブジェクトobj自身には新しい属性を追加できませんが、そのプロトタイプオブジェクトに新しい属性を追加することで、objでその属性を読み取ることができます。

この問題の解決策の一つは、objのプロトタイプも凍結することです。

var obj = new Object();
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);

proto.t = 'hello';
obj.t // undefined

もう一つの制限は、属性値がオブジェクトの場合、上記の方法は属性が指し示すオブジェクトを凍結するだけであり、オブジェクト自体の内容は凍結されないことです。

var obj = {
  foo: 1,
  bar: ['a', 'b']
};
Object.freeze(obj);

obj.bar.push('c');
obj.bar // ["a", "b", "c"]

上記のコードでは、obj.bar属性は配列を指し示しており、objオブジェクトが凍結された後も、この指し示しは変更できません。つまり、他の値を指し示すことはできませんが、指し示された配列自体は変更可能です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1