旧それなりブログの跡地、画像やスタイルやJSなどが壊れてることがあります。

[JavaScript] 配列をプロトタイプ・チェーン継承で拡張する

2012年2月4日

プロトタイプ・チェーンで配列型を拡張する理由

主な理由は以下の2つです

  • 1. JSのArray型は不便なので強化したい(拡張する理由)
  • 2. Array.prototypeを汚染したくない(Pチェーンで別クラスを作る理由)

1 は、まぁ説明不要だと思います
せめて each と indexOf(or inArray) くらいは無きゃ困るし
願わくば Rubyのcollectがあると断然読み易いコードになります
後はPythonのany/allなんかも良い
常に必要になる訳ではないので、ここぞという時に

var exArr = toExArray(arr);

こうやってパワーアップさせて処理できたら便利だなと

2 は、上記を解決するために prototype.js などの一部のライブラリは
ネイティブな Array.prototype を修正する手段を取っています
しかし、自分はどうしても気分的に気持ち悪さがあるのと
ここ数年の流れを見ていると
その様なコードやライブラリは好まれない傾向があるのとで
ネイティブ環境を書き換えない方法にしました
(for (i in arr) をやらなければ大丈夫なので、実害はあまり無いですけどね
 なお、Object型の拡張はダメ、ゼッタイ)

で、これらの解決手段として
 「Arrayをプロトタイプ・チェーンで継承して別クラスを作れば
  手軽に強化を実現出来ていいんじゃね?」
と思ったのがことの始まりです

サンプルコード

結論としては概ね期待通りの効果を得ることができました
以下、Arrayを強化したCollectionクラスの実装例です

var Collection = (function(){
    var cls = function(){

    // 自分を返すために必要
        this.__myClass__ = arguments.callee;
    }
    cls.prototype = new Array();

    //
    // Array既存メソッドを同じような振る舞いになるように上書き
    //
    cls.prototype.toString = function(){
        return Array.prototype.toString.apply(this.toArray(), arguments);
    }
    cls.prototype.slice = function(){
        return this.__myClass__.convert(Array.prototype.slice.apply(this, arguments));
    }
    cls.prototype.splice = function(){
        return this.__myClass__.convert(Array.prototype.splice.apply(this, arguments));
    }
    cls.prototype.concat = function(){
        throw new Error('Error in Collection.concat, not implemented');
    }

    //
    // 拡張
    //
    cls.prototype.toArray = function(){
        return Array.prototype.slice.apply(this);
    }

    cls.prototype.each = function(callback){
        var i;
        for (i = 0; i < this.length; i++) {
            var result = callback(this[i], i);
            if (result === true) {
                continue;
            } else if (result === false) {
                break;
            }
        }
    }

    cls.prototype.collect = function(callback){
        var newList = new this.__myClass__(), i;
        for (i = 0; i < this.length; i++) {
            var result = callback(this[i], i);
            if (result !== undefined) newList.push(result);
        }
        return newList;
    }

    cls.prototype.indexOf = function(target){// 無いブラウザの為に
        var i;
        for (i = 0; i < this.length; i++) { if (this[i] === target) return i };
        return -1;
    }

    cls.prototype.has = function(target){
        return this.indexOf(target) !== -1;
    }

    //
    // 生成関数群
    //   1) Collection.convert([])
    //   2) new Collection(引数無し)
    //   のどちらかを使う
    //
    //   new Collection([]) や Collection([]) は
    //   クラス毎にコンストラクタにベタでコードを書かないといけないので
    //   手軽でないのでNG
    //   new Array() の引数の意味とも合わないし
    //
    cls.convert = function(arr){
        if (arr instanceof Array === false) {
            throw new Error('Error in Collection.convert, invalid parameter');
        }
        var obj = new this(), i;
        for (i = 0; i < arr.length; i++) { obj.push(arr[i]) }
        return obj;
    }

    return cls;
})();

※Collectionを更に継承するとバグります
 __myClass__ の参照がサブクラスで共有されるから

ちょっと面倒だよねー

既存のArrayメソッド群の上書きを毎度書かないといけないのは面倒ですよねー
ってことで、それらをなじませる blendArray という関数を作りました

/**
 * Arrayをプロトタイプ・チェーン継承したクラスの
 * 継承されたメソッド群をなじませる
 *
 * 以下に記述のあるメソッドを対象とした
 * http://1106.suac.net/johoB/JavaScript/jscripta.html#array
 * Window7の IE8/Firefox3/Chrome で確認
 */
var blendArray = function(cls){
    /** 配列を自クラスインスタンスへ変換する */
    cls.convert = function(arr){
        if (arr instanceof Array === false) {
            throw new Error('Error in {YourArrayClass}.convert, invalid parameter');
        }
        var obj = new cls(), i;
        for (i = 0; i < arr.length; i++) { obj.push(arr[i]) };
            return obj;
    }

    /* 配列を返すメソッドは自クラスのインスタンスを返すように変更する */
    var toMine = function(methodName){
        return function(){
            return cls.convert(Array.prototype[methodName].apply(this, arguments));
        }
    }
    cls.prototype.slice = toMine('slice');
    cls.prototype.splice = toMine('splice');

    // これは解釈が分かれると思うのでエラーを返すのみとする
    cls.prototype.concat = function(){
        throw new Error('Error in {YourArrayClass}.concat, not implemented');
    }

    // 設定しないと使ったときにエラー
    cls.prototype.toString = function(){
        var arr = Array.prototype.slice.apply(this); // 配列に変換しないと使えない
        return Array.prototype.toString.apply(arr, arguments);
    }
}

こんな風に使う

var YourClass = function(){};
YourClass.prototype = new Array();
blendArray(YourClass);
// 以下にあなたの拡張を定義
YourClass.prototype.each = function(){}; // ...

convertが要らないなら、ローカル変数にするか
__convertDontTouchMe(+とても下品な単語) とかにリネームすればいいと思うヨ

参考

テストコードやその他メモ
プロトタイプ・チェーンを使ったArray型の拡張
blendArray

関連エントリ
プロトタイプ・チェーンで最も問題になる点