JavaScriptには様々な内部属性があり、それは二重ブラケット([[]]
)に囲まれて表現される。
プロパティも、いくつかの内部属性を持っている。
Object.getOwnPropertyDescriptor()とプロパティディスクリプタ
まずは、内部属性にアクセスする方法について。
プロパティの内部属性を取得するには、Object.getOwnPropertyDescriptor()
メソッドを使う。
第一引数に、対象となるオブジェクト、第二引数に、調べたいプロパティを渡す。
そうすると、内部属性について記述されたオブジェクトが返ってくる。このオブジェクトは、プロパティディスクリプタと呼ばれる。
プロパティディスクリプタにおいては、二重ブラケットはつかず、全て小文字で表現される。
また、内部属性のうち[[Enumerable]]
については、全てのオブジェクトで使用可能なメソッドであるpropertyIsEnumerable()
を使って確認することも可能。
調べたいプロパティを引数として渡すと、[[Enumerable]]
の状態(ture
かfalse
)が返ってくる。
var person = { name: 'Tom' }; var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor); // Object {value: "Tom", writable: true, enumerable: true, configurable: true} console.log(descriptor.enumerable); // ture console.log(person.propertyIsEnumerable('name')); // true
Object.getOwnPropertyDescriptor()
メソッドは、自身のプロパティのみを対象とする。
var person = { name: 'Tom' }; var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor); // Object {value: "Tom", writable: true, enumerable: true, configurable: true} console.log('toString' in person); // ture // person は toString を持っているが…… descriptor = Object.getOwnPropertyDescriptor(person, 'toString'); console.log(descriptor); // undefined // プロトタイプ継承されたものなので、Object.getOwnPropertyDescripotr()の対象にならない
EnumerableとConfigurable
全てのプロパティが共通して持っている内部属性は、[[Enumerable]]
と[[Configurable]]
の2つ。
どちらも、プロパティ作成時にはtrue
になっている。
内部属性を変更するには、Object.defineProperty()
を使う。
第一引数に、そのプロパティを持っているオブジェクト、第二引数に対象のプロパティ、第三引数にプロパティディスクリプタを渡す。
var person = { name: 'Tom' }; var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor.enumerable); // true console.log(descriptor.configurable); // true Object.defineProperty(person, 'name', { enumerable: false, configurable: false }); descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor.enumerable); // false console.log(descriptor.configurable); // false
[[Enumerable]]
は、列挙可能であるかどうかを設定する。
この値によってどのような違いが出るかは、次の記事を参照。
プロパティの操作,確認,列挙
[[Configurable]]
は、変更可能かどうかを設定する。
これがfalse
だと、そのプロパティを削除することは出来なくなる。また、一度false
にするとtrue
には戻せない。
値の変更は問題なく出来る。
[[Configurable]]
の挙動については、詳細を後述する。
var person = { name: 'Tom' }; var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor.configurable); // true Object.defineProperty(person, 'name', { configurable: false }); delete person.name; // 変更不可であるため、削除されない。strictモードだとここでエラーになる。 // 削除されていないので、引き続き参照できる console.log(person.name); // Tom // 値の変更は問題なく出来る person.name = 'Bob'; console.log(person.name); // Bob // 以下はエラーになる Object.defineProperty(person, 'name', { configurable: true });
ValueとWritable
プロパティには、データプロパティとアクセサプロパティの2種類がある。
詳細は下記の記事を参照。
アクセサプロパティ(getterとsetter)
[[Value]]
と[[Writable]]
は、データプロパティのみが持つ内部属性である。
[[Value]]
は値を格納し、[[Writable]]
は書き込み可能かどうかを設定する。
[[Writable]]
はデフォルトではtrue
に設定されている。
この状態だと、値はいつでも変更できる。
[[Writable]]
がfalse
の場合、値の変更は出来ず、strictモードでそれをやろうとした場合、エラーとなる。
var person = { name: 'Tom' }; var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor.value); // Tom console.log(descriptor.writable); // true person.name = 'Bob'; console.log(person.name); // Bob Object.defineProperty(person, 'name', { value: 'Ichiro' }); console.log(person.name); // Ichiro Object.defineProperty(person, 'name', { writable: false }); person.name = 'Tom'; // 書き込み不可になっているため、値の変更は行われない。strictモードだとエラーになる。 console.log(person.name); // Ichiro
[[Value]]
以外の内部属性の変更や、データプロパティからアクセサプロパティへの変更は、[[Writable]]
の状態とは無関係に行える。
var person = { name: 'Tom' }; console.log(person.name); // Tom Object.defineProperty(person, 'name', { writable: false }); console.log(person.propertyIsEnumerable('name')); // true // [[Writable]]がfalseでも、以下は問題なく行われる Object.defineProperty(person, 'name', { enumerable: false, get: function(){ return 'これはgetterです' } }); console.log(person.propertyIsEnumerable('name')); // false console.log(person.name); // これはgetterです
GetとSet
[[Get]]
と[[Set]]
は、アクセサプロパティのみが持つ内部属性。それぞれ、getter
関数とsetter
関数を格納する。
これについても詳細は、下記の記事を参照。
アクセサプロパティ(getterとsetter)
アクセサプロパティからデータプロパティへの変更
[[Configurable]]
がfalse
でない限り、アクセサプロパティをデータプロパティに変更することは出来る。
しかし、ただ単に値を代入しようとすると、プログラムはsetter
を呼び出そうとするため、上手くいかない。
Object.defineProperty()
メソッドで[[Value]]
を定義することで、データプロパティに変更できる。
var person = { get name(){ return 'これはgetterです' }, }; console.log(person.name); // これはgetterです // setterを呼び出す。この例ではsetterを定義していないため、何も行われない。strictモードだとエラーになる。 person.name = 'Bob'; // これは問題なく動作する Object.defineProperty(person, 'name', { value: 'Ichiro' }); console.log(person.name); // Ichiro
Configurableの挙動について整理
[[Configurable]]
がfalse
のときに出来ること
プロパティの種類がデータプロパティなら、以下の操作が可能。
- 値の代入
Object.defineProperty()
による[[value]]
の操作[[Writable]]
をtrue
からfalse
に変える(逆は不可)
[[Configurable]]
がfalse
のときに出来ないこと
[[Enumerable]]
や[[Configurable]]
の操作[[Writable]]
をfalse
からtrue
に変える(逆は可能)delete
によるプロパティの削除(何も起こらない。strictモードだとエラーになる。)- データプロパティからアクセサプロパティに変更
- アクセサプロパティからデータプロパティに変更
getter
やsetter
の上書きや追加
// データプロパティのケース var person = {}; Object.defineProperty(person, 'name', { value: 'Tom', configurable: false, enumerable: true, writable: true }); // 動作する person.name = 'Bob' console.log(person.name); // Bob // 動作する Object.defineProperty(person, 'name', { writable: false }); var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); console.log(descriptor.writable); // false // エラーになる Object.defineProperty(person, 'name', { writable: true }); // エラーになる Object.defineProperty(person, 'name', { get: function(){ return } });
// アクセサプロパティのケース var person = {}; Object.defineProperty(person, 'name', { get: function(){ return 'これはgetterです。' }, configurable: false, enumerable: true, }); console.log(person.name); // これはgetterです。 // エラーになる Object.defineProperty(person, 'name', { value: 'Bob' }); // エラーになる Object.defineProperty(person, 'name', { get: function(){ return '名前' } }); // エラーになる Object.defineProperty(person, 'name', { set: function(value){ return value; } });
Object.defineProperty()によるプロパティの追加
Object.defineProperty()
の第二引数に指定したプロパティが存在しなかった場合、新しくその名前のプロパティを追加する。
その際に設定していなかった内部属性は、false
となる。
ちなみに、内部属性を何も設定せずにObject.defineProperty()
でプロパティを追加すると、[[Value]]
がundefined
のデータプロパティが作成される。
var person = {}; Object.defineProperty(person, 'name', { value: 'Tom', enumerable: true }); console.log('name' in person); // true console.log(person.name); // Tom var descriptor = Object.getOwnPropertyDescriptor(person, 'name'); // Object {value: "Tom", writable: false, enumerable: true, configurable: false} console.log(descriptor); // 明示的に設定していなかったwritableとconfigurableはfalseになっている Object.defineProperty(person, 'age', { }); console.log('age' in person); // true console.log(person.age); // undefined descriptor = Object.getOwnPropertyDescriptor(person, 'age'); // Object {value: undefined, writable: false, enumerable: false, configurable: false} console.log(descriptor);
複数のプロパティを一度に定義
Object.defineProperties()
を使うことで、複数のプロパティを一度に定義することが出来る。
第一引数に対象となるオブジェクト、第二引数に、プロパティ名とプロパティディスクリプタの組み合わせのオブジェクト、を渡す。
機能的には、Object.defineProperty()
と全く同じ。
var person = { _name: 'Tom' }; console.log(person._name); // Tom Object.defineProperties(person, { _name:{ value: 'Bob' }, name:{ get: function(){ return '名前は'+this._name; }, set: function(value){ this._name = 'Mr.'+value; } } }); console.log(person._name); // Bob person.name = 'Ichiro'; console.log(person._name); // Mr.Ichiro console.log(person.name); // 名前はMr.Ichiro