プロパティの内部属性

JavaScriptには様々な内部属性があり、それは二重ブラケット([[]])に囲まれて表現される。
プロパティも、いくつかの内部属性を持っている。

Object.getOwnPropertyDescriptor()とプロパティディスクリプタ

まずは、内部属性にアクセスする方法について。

プロパティの内部属性を取得するには、Object.getOwnPropertyDescriptor()メソッドを使う。
第一引数に、対象となるオブジェクト、第二引数に、調べたいプロパティを渡す。
そうすると、内部属性について記述されたオブジェクトが返ってくる。このオブジェクトは、プロパティディスクリプタと呼ばれる。
プロパティディスクリプタにおいては、二重ブラケットはつかず、全て小文字で表現される。

また、内部属性のうち[[Enumerable]]については、全てのオブジェクトで使用可能なメソッドであるpropertyIsEnumerable()を使って確認することも可能。
調べたいプロパティを引数として渡すと、[[Enumerable]]の状態(turefalse)が返ってくる。

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()の対象にならない

EnumerableConfigurable

全てのプロパティが共通して持っている内部属性は、[[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
});

ValueWritable

プロパティには、データプロパティとアクセサプロパティの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です

GetSet

[[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モードだとエラーになる。)
  • データプロパティからアクセサプロパティに変更
  • アクセサプロパティからデータプロパティに変更
  • gettersetterの上書きや追加
// データプロパティのケース
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