[JavaScript] 6. Object.defineProperty/ies でプロパティ/メソッド定義

---

1. [クラスの定義](http://qiita.com/Koizumi-Greenwich/items/7a579c43084286dd38be)
2. [クラスの継承](http://qiita.com/Koizumi-Greenwich/items/96215899b8b61cc85e65)
3. [即時関数 と jQuery.readyイベント関数](http://qiita.com/Koizumi-Greenwich/items/d192ad98ca8b2449e4fd)
4. [即時関数でクラス定義](http://qiita.com/Koizumi-Greenwich/items/6f74230f35f2c5b9a46b)
5. [private プロパティ](http://qiita.com/Koizumi-Greenwich/items/f9e8a6dad4f049c89944)
6. [Object.defineProperty/ies でプロパティ/メソッド定義](http://qiita.com/Koizumi-Greenwich/items/1b827b4304538f2f8e37)

---

[5.private プロパティ](http://qiita.com/Koizumi-Greenwich/items/f9e8a6dad4f049c89944) で解説した内容と重複しますが、Object.defineProperty/ies を用いたプロパティ、メソッド定義について、より詳しく解説します。

#Object.defineProperty() / Object.defineProperties()

Object.defineProperty/ies は、オブジェクトにプロパティをセットする関数です。
クラス定義では動的プロパティ、またはメソッドを定義するために用いられます。

詳細については [MDN Object.defineProperty()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)、および [MDN Object.properties()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties) を参照してください。

##Object.defineProperty()

オブジェクトに対して **プロパティを1つ** セットします。
既存のプロパティが指定された場合は上書き・変更されます。

```text:Object.defineProperty()
Object.defineProperty(オブジェクト, プロパティ/関数名, {パラメータ, ...});
```

1つ目の引数は、プロパティをセットする対象オブジェクトです。
2つ目の引数は、セットするプロパティの名前（文字列）です。セットされる内容が関数オブジェクトの場合は *関数名*になります。
3つ目の引数では、プロパティの内容＝デスクリプタをハッシュオブジェクトとして渡します。

###デスクリプタ
プロパティの内容や機能、設定を行うパラメータです。以下のキー・要素を持つハッシュオブジェクトで指定します。

- value
	- プロパティの値を指定します。文字列、整数などの値のほか、配列、ハッシュ、関数などのオブジェクトも渡すことができます。
- writable
	- true を指定するとプロパティ値の変更が可能になります。デフォルトは false なので変更不可です。
- set
	- セッター関数を定義します。ここで指定された関数は、プロパティに値を代入する際に呼び出されて処理されます。
- get
	- ゲッター関数を定義します。ここで指定された関数は、プロパティ値を参照、取得しようとした際に呼び出されて処理されます。
- enumerable
	- console.log()などでオブジェクト内容の一覧を取得した際、このプロパティが表示されるかどうかを決めます。デフォルトは false で表示されません。

##Object.defineProperties()

オブジェクトに対して **複数のプロパティ** をセットします。
同様に既存のプロパティが指定された場合は上書き・変更されます。

```text:Object.defineProperty()
Object.defineProperty(オブジェクト, {プロパティ/関数名: {パラメータ, ...}, .... });
```

1つ目の引数は、プロパティをセットする対象オブジェクトです。
2つ目の引数では、プロパティ名（または関数名）とデスクリプタのペアをハッシュオブジェクトで渡します。

##コンストラクタ内での使用例
以下は、実際にコンストラクタ内でプロパティとメソッドを定義する例です。

```js:defprop.js
MyClass = (function() {
    // コンストラクタ
    var myClass = function(prop1, prop2) {
        // (1) privateプロパティを格納するハッシュオブジェクト
        var props = {};
        
        // (2) プロパティ prop1 に コンストラクタ引数 prop1 をセットする
        Object.defineProperty(this, "prop1", {value: prop1});
        
        Object.defineProperties(this, {
            // (3) プロパティ prop2 に コンストラクタ引数 prop2 をセットする
            prop2: {value: prop2, writable: true},
            // (4) メソッド methodA の定義
            //   ※ デスクリプタを返すコールバック関数
            //   ※ private プロパティ が扱えるように引数で渡す
            methodA: function(props) {
                // (5) デスクリプタを返す
                return {
                    // (6) メソッド本体の定義
                    value: function(arg) {
                        return this.prop1 + this.prop2 + props.pri1 + arg;
                    }
                }
            },
            // (7) private プロパティ pri1 のアクセサ定義
            //   ※ デスクリプタを返すコールバック関数
            //   ※ private プロパティ が扱えるように引数で渡す
            pri1: function(props) {
                // (8) デスクリプタを返す
                return {
                    // (9) セッター関数の定義
                    set: function(value) {
                        props.pri1 = value;
                    },
                    // (10) ゲッター関数の定義
                    get: function() {
                        return props.pri1;
                    }
                };
            },
        });
    }
    
    return myClass;
})();
```

(1) private プロパティを格納するためのハッシュオブジェクトを宣言、
初期化しています。複数の private プロパティを扱う場合はハッシュオブジェクト化しておくと便利です。

(2) Object.defineProperty を用いて、単一のプロパティ **prop1** を定義しています。
デスクリプタでは、 **value** パラメータにてコンストラクタの引数 *prop1* の値をセットしています。
なお、**writable** パラメータが指定されていないので、このプロパティは変更ができなくなります。すなわち実質的にクラス内定数となります。

(3) Object.defineProperties を用いて複数のプロパティ、メソッドを定義している例です。
ここではプロパティ **prop2** を遅疑しています。
**value** パラメータにてコンストラクタ引数 *prop2* の値をセットし、**writable** で変更可にしています。

(4) メソッド **methodA** の定義です。
ただし、デスクリプタを直接指定するのでは無く、 **コールバック関数を介して**デスクリプタ値（ハッシュオブジェクト）を返すようにしています。
わざわざこのような手順を踏むのは、 **メソッド処理内で private プロパティ を扱えるようにする**ためです。コールバック関数の引数に private プロパティ（またはそれを含むオブジェクト）を渡すことで、コールバック関数内でも private プロパティ にアクセスできるようになります。
ここではコールバック関数に private プロパティを格納しているローカル変数 **props** を渡しています。

(5) コールバック関数の返り値としてデスクリプタのハッシュオブジェクトを返しています。

(6) デスクリプタの **value** パラメータに、メソッドとなる関数オブジェクトをセットしています。
メソッド **methodA** が呼び出された際に実際に処理を行うのが、ここで定義する関数になります。

(7) private プロパティ **pri1** の定義と、そのアクセサの定義です。
メソッドの定義と同様に、デスクリプタ値を返すコールバック関数として定義します。

(8) コールバック関数の返り値としてデスクリプタのハッシュオブジェクトを返しています。

(9) private プロパティ **pri1** に対するセッター関数を定義しています。
**pri1** に値を代入しようとすると、ここで定義した関数が処理を行います。
引数には **代入しようとした値**が渡されます。
ここでは **pri1** に代入しようとした値を、ローカルな *props* ハッシュオブジェクトのキー `pri1` にセットしています。

(10) private プロパティ **pri1** のゲッター関数を定義しています。
**pri1** の値を参照しようとすると、この関数が処理を行います。
ここでは ローカルな *props.pri1* の値を返すようにしています。

#関数定義はコンストラクタの外に
[5.private プロパティ](http://qiita.com/Koizumi-Greenwich/items/f9e8a6dad4f049c89944)でも触れましたが、関数オブジェクト内で関数定義を記述するのは、メモリコストの増大やメモリーリークなどのリスクがあり、望ましくないとされています。
したがって、関数定義部分はコンストラクタの外で行うようにします。

前述の例では、コールバック関数部分をコンストラクタの外で定義することでより望ましい形に書き換えられます。

```js:defprop.js
MyClass = (function() {
    // コンストラクタ
    var myClass = function(prop1, prop2) {
        // privateプロパティを格納するハッシュオブジェクト
        var props = {};
        
        // プロパティ prop1 に コンストラクタ引数 prop1 をセットする
        Object.defineProperty(this, "prop1", {value: prop1});
        
        Object.defineProperties(this, {
            // プロパティ prop2 に コンストラクタ引数 prop2 をセットする
            prop2: {value: prop2, writable: true},
            // メソッド methodA の定義
            //   ※ private プロパティ が扱えるように引数で渡す
            methodA: defineMethodA(props),
            // private プロパティ pri1 のアクセサ定義
            //   ※ private プロパティ が扱えるように引数で渡す
            pri1: definePri1(props),
        });
    }
    
    // メソッド methodA を定義するデスクリプタを返すコールバック関数
    //   ※ private プロパティ を引数で受け取る
    var defineMethodA = function(props) {
        // (5) デスクリプタを返す
        return {
            // (6) メソッド本体の定義
            value: function(arg) {
                return this.prop1 + this.prop2 + props.pri1 + arg;
            }
        }
    }
            
    // private プロパティ pri1 のアクセサを定義するデスクリプタを返すコールバック関数
    //   ※ private プロパティ を引数で受け取る
    var definePri1 = function(props) {
        // デスクリプタを返す
        return {
            // セッター関数の定義
            set: function(value) {
                props.pri1 = value;
            },
            // ゲッター関数の定義
            get: function() {
                return props.pri1;
            }
        };
    }
    
    return myClass;
})();
```

#全体のサンプルコード
クラスの継承も含めたサンプルコードです。

```html:defprop.html
<!DOCTYPE html>
<script>
/**
 * クラス MyClass
 */
MyClass = (function() {
    // コンストラクタ
    var myClass = function(prop1, prop2) {
        // privateプロパティを格納するハッシュオブジェクト
        var props = {};
        
        // プロパティ prop1 に コンストラクタ引数 prop1 をセットする
        Object.defineProperty(this, "prop1", {value: prop1});
        
        Object.defineProperties(this, {
            // プロパティ prop2 に コンストラクタ引数 prop2 をセットする
            prop2: {value: prop2, writable: true},
            // メソッド methodA の定義
            //   ※ private プロパティ が扱えるように引数で渡す
            methodA: defineMethodA(props),
            // private プロパティ pri1 のアクセサ定義
            //   ※ private プロパティ が扱えるように引数で渡す
            pri1: definePri1(props),
        });
    }
    
    // メソッド methodA を定義するデスクリプタを返すコールバック関数
    //   ※ private プロパティ を引数で受け取る
    var defineMethodA = function(props) {
        // (5) デスクリプタを返す
        return {
            // (6) メソッド本体の定義
            value: function(arg) {
                return this.prop1 + this.prop2 + props.pri1 + arg;
            },
        }
    }
            
    // private プロパティ pri1 のアクセサを定義するデスクリプタを返すコールバック関数
    //   ※ private プロパティ を引数で受け取る
    var definePri1 = function(props) {
        // デスクリプタを返す
        return {
            // セッター関数の定義
            set: function(value) {
                props.pri1 = value;
            },
            // ゲッター関数の定義
            get: function() {
                return props.pri1;
            }
        };
    }
    
    return myClass;
})();

// サブクラス MySubClass
MySubClass = (function(){
    // コンストラクタ
    var mySubClass = function(prop1, prop2, prop3) {
        // 親クラスのコンストラクタを呼ぶ
        MyClass.call(this, prop1, prop2);
        
        // プロパティ、メソッドの定義
        Object.defineProperties(this, {
            prop3: {value: prop3, writable: true},
            methodB: defineMethodB(), 
        });
    }
    // MyClass を継承
    mySubClass.prototype = Object.create(MyClass.prototype, {value: {constructor: mySubClass}});
    
    // メソッド methodB の定義
    var defineMethodB = function() {
        return {
            value: function(arg) {
                return this.methodA(arg) + this.prop3 + arg;
            }
        }
    }
    
    return mySubClass;
})();

// MyClassオブジェクトを生成
var obj = new MyClass('aaa', 'bbb');
// pri1に値をセット
obj.pri1 = "hoge"
// MyClass.methodA() を呼び出す
var result = obj.methodA('ccc');

document.write(result + "<hr>");

var subobj = new MySubClass('mmm', 'nnn', 'ooo');
// pri1に値をセット
subobj.pri1 = "hoge"
// MySubClass.methodA() を呼び出す
var result = subobj.methodB('ppp');

document.write(result + "<hr>");
</script>
```