JavaScript のクラス定義を他のオブジェクト指向言語っぽく僕なりにアレンジしてみた

もう ECMAScript の規格でクラス定義の文法が進んでるのでこんなまどろっこしいことはしなくてもよくなりそうですが(
まあそれが普及するまでのやり方の一つとして。

# 解説

`defineClass`というクラス定義用の関数を作りました。（文末に添付）
…こんなことするくらいならもう GitHub 作っちゃえよ、とも思いますが(
ちょっとアカウント管理の仕方に問題があってリポが作れずにいます。
`defineClass`にスーパークラスと、クラス定義をまとめたオブジェクト（連想配列）を渡すと
`defineClass`関数の中で `Object.defineProperties` 用のディスクリプターに整形してコンストラクター用の関数に設定してくれるというものです。

# 使用例

例えば先日投稿した Modified-UTF7 のエンコーダーならこんな感じで定義します。

```js:modifiedUTF7.js
defineClass(null, {
    '': MUTF7 = function(){},
    'encode': ['static function', function(plain_str) {
        return plain_str
            .replace(/&/g, '&-')
            .replace(/[\0- \u007f-\uffff]+/g, function(str) {
                return '&' + Base64.encode(str) + '-';
            });
    }],
    'decode': ['static function', function(utf7_str) {
        return utf7_str
            .replace(/&([A-Za-z0-9\+\/]+=*)-/g, function(str, $1) {
                return Base64.decode($1);
            }).replace(/&-/g, '&');
    }]
});
```

`defineClass(null, {` の最初の `null` はスーパークラスです。
今回は何も継承しないので `null` にしてますが、
例えば `Foo` というクラスを継承したい場合は (`Foo.prototype` ではなく) `Foo` を渡します。
次の `'': MUTF7 = function(){},` はコンストラクター定義です。
`''` (空文字列) というキーに関数を入れると、それを `defineClass` 関数がコンストラクターとみなして、こいつに対して `Object.defineProperties` します。
わかりにくいですが、同時に `MUTF7` という変数にコンストラクターを代入してるので、定義してるクラスの名前は `MUTF7` になります。
`'encode': ['static function', function(plain_str)` の辺りは関数名と修飾子です。
プロパティはざっくり以下の文法で定義します。関数の場合は値のところに関数オブジェクトを入れます。

```js
{
    'プロパティ名': ['修飾子', 値]
}
```

ここでは `encode` という名前の静的関数を定義してます。 `decode` 関数も同様です。

## getter/setter の定義例

```js:BitQueue.js
defineClass(null, {
    '': BitQueue = function(){},
    'value_': ['hidden', 0],
    'length_': ['hidden', 0],
    'length': ['property', {
        'get': function() {
            return this.length_;
        }
    }],
    /* 中略 */
});
```

こっちのクラスには value_, length_ という隠しプロパティと length という getter が定義されてます。
プロパティは以下のような感じで、修飾子に property を入れて値のところに geter/setter を入れます。片方だけもおｋ。

```js
{
    'プロパティ名': ['property', {
        'get': function() {
            /* getter */
        },
        'set': function(value) {
            /* setter */
        }
    }]
}
```

# 修飾子一覧

修飾子は以下からいくらでも選んでスペースで区切るか、配列に入れます。

|修飾子|説明|
|:--|:--|
|static|静的|
|hidden (別名: private)|`for` ... `in` 文で列挙されない|
|const (別名: final, readonly)|読み取り専用|
|property (別名: prop)|プロパティ (値のところに getter/setter を定義)|
|function (別名: func, method)|メソッド (※`hidden const` と同義)|

property を使う場合 const は無視されます。

# ソースコード (defClass.js)

```js:defClass.js
//---------------------------------------------------------------------------
// ● クラス定義
//     superClass : (function) スーパークラス
//     members    : (object)   メンバー
//     {
//         // コンストラクター (名前が空文字列)
//         '': function(/*引数*/) { /*...*/ }
//         // フィールド
//         '名前': ['', 初期値]
//         // フィールド (初期値なし)
//         '名前': ['', undefined]
//         // 静的フィールド (クラス名.フィールド名 でアクセス)
//         '名前': ['static', 初期値]
//         // 定数 (変更できない; static 併用可)
//         '名前': ['const', 値]  //<- final や readonly でもおｋ
//         // hiddenメンバー (for...in 等で列挙されない; static,const 併用可)
//         // ※列挙されないだけで参照は外からもできます
//         '名前': ['hidden', 初期値]  //<- private でもおｋ
//         // 関数 (hidden const のエイリアス)
//         '名前': ['method', function(/*引数*/) { /*...*/ }]
//         // プロパティ (static,const,hidden 併用可)
//         '名前': ['property', {'get': function(){}, 'set': function(value){}}]
//     }
//---------------------------------------------------------------------------
function defineClass(superClass, members) {
    // コンストラクター
    var theClass = members[''] || function(){};
    delete members[''];

    // メンバー
    var descriptors = {};
    var staticDescriptors = {};
    for(var memberName in members) {
        var memberDefinition = members[memberName];
        var info = memberDefinition[0];

        // 修飾子定義を文字列の配列に分割
        if(typeof info === 'string') {
            info = info.trim();
            info = info !== '' ? info.split(/\s+/) : [];
        }

        // 修飾子の処理
        var isStatic = false;       // 静的
        var isConst = false;        // 読み取り専用
        var isEnumerable = true;    // 列挙可
        var isProperty = false;     // プロパティか否か
        for(var i = 0; i < info.length; ++i) {
            var def = info[i];

            if(def === 'static') {
                isStatic = true;
            }
            else if(def === 'const' ||
                    def === 'final' ||
                    def === 'readonly') {
                isConst = true;
            }
            else if(def === 'hidden' ||
                    def === 'private') {
                isEnumerable = false;
            }
            else if(def === 'prop' ||
                    def === 'property') {
                isProperty = true;
            }
            else if(def === 'func' ||
                    def === 'function' ||
                    def === 'method') {
                isConst = true;
                isEnumerable = false;
            }
            else if(def) {
                throw new RangeError('Invalid modifier: ' + def);
            }
        }

        // プロパティ定義まとめ
        var descriptor = {
            'configurable': false,
            'enumerable': isEnumerable
        };
        if(isProperty) {
            descriptor['get'] = memberDefinition[1]['get'];
            descriptor['set'] = memberDefinition[1]['set'];
        }
        else {
            descriptor['writable'] = !isConst;
            descriptor['value'] = memberDefinition[1];
        }

        // 追加
        (isStatic ? staticDescriptors : descriptors)[memberName] =
            descriptor;
    }

    // 継承
    if(superClass) {
        theClass.prototype.prototype = superClass.prototype;
    }

    // プロパティを適用
    Object.defineProperties(theClass, staticDescriptors);
    Object.defineProperties(theClass.prototype, descriptors);

    return theClass;
}
```