JavaScriptのプロパティディスクリプタ 〜 JSおくのほそ道 #019

こんにちは、ほそ道です。

今回は前回のGetter/Setterと関連深いプロパティディスクリプタを取り上げます。
プロパティディスクリプタとはオブジェクトのプロパティのメタ属性でデータアクセスに関する取り決めを保持しています。
[MDNのjavascript1.8.5のドキュメント](https://developer.mozilla.org/ja/docs/Web/JavaScript/New_in_JavaScript/1.8.5)にまとまっていますが、ECMA5ではディスクリプタを設定してプロパティを制御するメソッドが沢山追加されています。
これはECMA5以降の仕様となっているようです。

[目次はこちら](http://qiita.com/setzz/items/ad9a9c99a4fda041a563)

# Object.getOwnPropertyDescriptor

ディスクリプタを見ていくにあたって、とりあえずgetOwnPropertyDescriptorというメソッドを動かしてみます。このメソッドはオブジェクトとプロパティを指定して、そのディスクリプタを取得できます。

```js:とあるオブジェクト
var man = (function() {
  var _age = 0;
  return {
    get age() {
      return _age;
    },
    set age(val) {
      _age = val;
    }
  }
}());
```

前回取り上げたようなGetter/Setterを持つオブジェクトです。
それでは`age`プロパティのディスクリプタを取得してみましょう。

```js:getOwnPropertyDescriptorその１
man.age = 100;
var descriptor = Object.getOwnPropertyDescriptor(man, 'age');
console.log(descriptor);
// 出力
// {get: [Function: age],
//  set: [Function: age],
//  enumerable: true,
//  configurable: true }
```

今度はフツーにオブジェクトリテラルで宣言したプロパティのディスクリプタを取得してみましょう。

```js:getOwnPropertyDescriptorその２
var foo = {
  bar: 100
};

var descriptor = Object.getOwnPropertyDescriptor(foo, 'bar');
console.log(descriptor);

// 出力
// {value: 100,
//  writable: true,
//  enumerable: true,
//  configurable: true }
```

ディスクリプタの内容が先ほどと異なりますね。
この違いについては下記にまとめます。

# アクセサディスクリプタとデータディスクリプタ

getやsetという属性を持つディスクリプタの事を**アクセサディスクリプタ**といいます
Getter/Setterを設定されたプロパティはこちらのディスクリプタとなります。

valueやwritableという属性を持つディスクリプタの事を**データディスクリプタ**といいます。
オブジェクトリテラルにおいて`{kei:value}`の形式で記述されたプロパティはこちらのディスクリプタになります。

言葉だけ覚えておくと資料読みの際等に役にたつかもしれません。

# ディスクリプタのメタ属性

下記のモノで全てです。名前もわかり易いので一度使えば覚えられるレベルです。
writable属性をfalseにすることで定数を作る事もできますね。

|     名前     |  規定値   |                                  内容                           |
|--------------|-----------|----------------------------------------------------------------|
| configurable | false     | trueの時、ディスクリプタ変更や、オブジェクトからプロパティ削除が可能 |
| enumerable   | false     | trueの時、そのプロパティはkeysやfor...in...によるプロパティ列挙に現れる|
| value        | undefined | プロパティの値。データディスクリプタのみ。                       |
| writable     | false     | trueの時、値を変更可能。データディスクリプタのみ。        |
| get          | undefined | Getter関数。アクセサディスクリプタのみ。                                |
| set          | undefined | Setter関数。アクセサディスクリプタのみ。                     |

# Object.defineProperty

この関数はプロパティとディスクリプタを設定できます。

```js:データディスクリプタを持つプロパティを生成
'use strict';
var man = {};
Object.defineProperty(man, 'age', {
  value: 20,
  writable: false,
});

man.age = 30;  // TypeError: Cannot assign to read only property 'age' of #<Object>
```

writableをfalseにして上書きしようとすると通常は無視され、strictモードにおいてはTypeErrorとなります。
Getterのみを設定したときと同じですね。

今度はアクセサディスクリプタを持つプロパティを生成してみましょう。

```js:アクセサディスクリプタを持つプロパティを生成
var man = {
  _age: 0
};
Object.defineProperty(man, 'age', {
  get: function() {
    return this._age;
  },
  set: function(val) {
    this._age = val;
  },
});
```

__defineSetter__, __defineGetter__の時同様カプセル化が出来ておらず_age変数に直接アクセス出来てしまうのが相変わらず気になっていますが。。

このほかにも一度に複数のプロパティを設定できる`Object.defineProperties`というメソッドもあります。

# Object.create

Object.createは引数を二つとって新しいオブジェクトを返します。
第一引数はオブジェクトの__proto__となるオブジェクト。
第二引数はプロパティを列挙したオブジェクトです。プロパティの値はディスクリプタとなります。

```js:createメソッド
var obj = Object.create({
  name: 'foo'
}, {
  age: {
    value: 20,
    writable: true
  }
});
console.log(obj.name);  // foo
console.log(obj.age);   // 20
```

# その他のディスクリプタ操作

その他にはディスクリプタ一括変更とその状態確認の処理があります。

|        名前       |                              内容                             |
|-------------------|---------------------------------------------------------------|
| preventExtensions | オブジェクトの拡張を禁止                        |
| isExtensible      | オブジェクトの拡張が許可されているか                   |
| seal              | オブジェクトの全プロパティの削除を禁止                |
| isSealed          | オブジェクトが封印(seal)されているか              |
| freeze            | オブジェクトの全プロパティに対して変更/削除を禁止する |
| isFrozen          | オブジェクトが凍結(freeze)されているか                 |

試しに一番強制力の強いfreezeメソッドを見てみましょう

```freezeメソッド
var man = {
  name: 'hosomichi',
  age: 20
};
Object.freeze(man);
man.age = 30;
man.address = 'Tokyo';
console.log(man.age);     // 20
console.log(man.address); // undefined
```

変更と拡張は無視されました。
strictモードだと下記のようなエラーが発生します。

- 変更時
   `TypeError: Cannot assign to read only property 'age' of #<Object>`  

- 拡張時
   `TypeError: Can't add property address, object is not extensible`


# まとめ
正直、いちいちディスクリプタを設定してられないと思うので
全部のオブジェクトにこれをやる必要は無いと思います。
しかし奔放すぎるJavaScriptのプロパティに対して結構な制御が可能となり、
下記のような場合は使えるのではないでしょうか。

- 不変オブジェクトを造りたい！などオブジェクトの状態を保証したい場合
- 定数を作りたい場合
- 一部のプロパティをfor...in...などから隠蔽したい場合(enumerableがfalseの場合、for...in...ループでは登場しなくなりますが、`Object.getOwnPropertyNames(object)`を使用する事でプロパティ名の配列が取得出来ます。

今回は以上です。