JavaScript の Array オブジェクトを汚染させずに拡張してみる

前回のエントリ「jQuery オブジェクトを汚染せずにプラグインを追加する方法を考えてみた」ではラッパーオブジェクトをかぶせることで、jQuery オブジェクトを汚染させずに機能拡張(prototype オブジェクトへのメソッドの追加)してみましたが、同様の方法を使えば、String、Date、Array といったJavascriptの標準のオブジェクトも汚染させずに機能拡張できるのではと思い試してみました。

prototypeの拡張によるメリット、デメリット

メリット

例えばArray オブジェクトの場合、「指定した値を持つ配列行の索引番号を返す」というメソッドを持ってませんが、以下のように prototype を拡張することで実現できます。

Array.prototype.index=function(val){
    var arr=this,len=-1;
    while(len<arr.length)if(arr[++len]==val)return len;
    return len;
}

var test=['aaa','bbb','ccc']
alert(test.index('bbb')) //1

専用の関数を作って、
array_index(test,'bbb')
とか書かなくて済みます。

デメリット

prototype が汚染されてないことを前提に処理してるロジック(ライブラリなど)で問題が発生することが多いようです。

不特定多数の人が使用するライブラリなどにおける prototype の独自拡張はお行儀の悪い行為とみなされるようです。

Array オブジェクト

ラッパーオブジェクトを使用し、先のデメリットを解消し、メリットのみを享受できるようにしてみます。
とりあえず Array で試そうと思うので、Array の基本機能、応用処理をおさらいしてみます。

基本
var arr=['aaa','bbb','ccc']

//連結文字列化
alert(arr.join()) //aaa,bbb,ccc
alert(arr.join('')) //aaabbbccc

//抽出
alert(arr.slice(1,3)) //bbb,ccc

//ソート
alert(arr.reverse()) //ccc,bbb,aaa
alert(arr.reverse().sort())  //aaa,bbb,ccc

//先頭の配列削除
arr.shift() 
alert(arr) //bbb,ccc

//先頭に追加
arr.unshift('xxx') 
alert(arr) //xxx,aaa,bbb,ccc

//指定位置削除
arr.splice(1,1)
alert(arr) //aaa,ccc

//指定位置抜き取り
alert(arr.slice(1,3)) //bbb,ccc

//指定位置の削除
arr.splice(1,1)
alert(arr) //aaa,ccc

//指定位置の削除、置き換え
arr.splice(1,1,['x','y'])
alert(arr) //aaa,x,y,ccc

//配列結合
alert(arr.concat(['ddd','eee'])) //aaa,bbb,ccc,ddd,eee

//最後尾に値追加
arr.push('ddd')
alert(arr) //aaa,bbb,ccc,ddd
arr.push(['xxx','yyy'])
alert(arr) //aaa,bbb,ccc,ddd,xxx,yyy

//最後尾の値取得
alert(arr.pop()) //ccc

文字列を配列にする

var arr='aaa,bbb,ccc'.split(','),i=0;
while(i<arr.length)alert(arr[i++]) // aaa → bbb → ccc
応用

id:amachang のこちら記事を参考に arguments を Array に変換する方法を確認。

arguments を配列に変換するクロスブラウザな三つの方法を考えてベンチマークを取ってみました。

arguments に対して shift するための考察(をしていたらカッとなって配列変換のベンチマーク)- IT戦記

arguments に Array のメソッドを適用する方法
注)この方法だと「Opera 9 にはバグがあって shift.apply(arguments) に失敗して、 arguments を壊してしまいます」との事です。

(function(){
    var shift = Array.prototype.shift;
    var join = Array.prototype.join;
    shift.apply(arguments);
    alert(join.apply(arguments)); //bbb,ccc
})('aaa','bbb','ccc')

arguments を配列に置換する方法

(function(){
    arr=[]

    //push
    arr.push.apply(arr,arguments);
    alert(arr.join()) //aaa,bbb,cc

    //concat
    arr=arr.concat.apply(arr,arguments);
    alert(arr.join()) //aaa,bbb,cc

})('aaa','bbb','ccc')

ラッパーオブジェクトによる拡張

以下の記述で拡張してみます。前半部が「ラッパーオブジェクト生成の汎用処理」で後半部が「Array オブジェクトの拡張」となってます。他のオブジェクト(String や Date など)を拡張する際は、後半部のみの記述で済みます。

var $js={}

//ラッパーオブジェクト生成の汎用処理
$js.fnc=function(oName,name,args){
    var arr=[],o=args[0];
    arr.push.apply(arr,args)
    arr.shift()
    return $js[oName].prototype[name].apply(o,arr)
}
$js.callFnc = function(oName,name,arguments){
    return function(){
        return $js.fnc(oName,name,arguments)
    }
}
$js.setFnc = function(oName,name){
    if(arguments.length<2){
        for(var name in $js[oName].prototype){
            $js[oName][name]=$js.callFnc(oName,name,arguments)
        }
    }
    else return $js.callFnc(oName,name,arguments)
}
$js.bindWrap = function(wrap,obj){
    for(var i in wrap)
        (function(i){
            if(wrap[i] instanceof Function)
                obj[i]=function(){
                    return wrap[i].apply(wrap,arguments)
                }
        })(i)
}

//Array オブジェクトの拡張
$js.Array=function(){
    var o=this;
    o._arr=[];
    this.convert(arguments)
    $js.bindWrap(o,o._arr);
    return this._arr;
}
$js.Array.prototype.convert=function(args){
    var arr=this._arr||this;
    for(var i=0;i<args.length;i++){
        var arg=args[i]
        if(!(args[i] instanceof Array)){
            arg=[args[i]]
        }
        arr.push.apply(arr,arg)
    }
    return arr;
}
$js.Array.prototype.index=function(val){
    var arr=this._arr,len=-1;
    while(len<arr.length)if(arr[++len]==val)return len;
    return len;
}
$js.Array.prototype.each=function(f){
    var arr=this._arr;
    for(var i=0;i<arr.length;i++)f.apply(arr,[i])
    return arr;
}
$js.Array.prototype.remove=function(){
    var arr=this._arr
    var key = new $js.Array($js.Array.convert([],arguments)),len=arr.length,idx;
    while(len--){
        if(idx=key.index(arr[len])>-1)arr.splice(len,1)
    }
    return arr;
}

//copy $js.Array.prototype.xxx to $js.Array.xxx
$js.setFnc('Array')

$js.setFnc('Array')では、$js.Array.prototypeで定義したメソッド郡を、$js.Arrayに(引数渡しを細工して)コピーしています。具体的には以下のような感じ。

定義

$js.Xxx.prototype.aaa=function(msg){
    //…
}
$js.setFnc('Xxx')
   ↓この定義を自動でやってくれる
//$js.Xxx.aaa=function(msg){
//    //…
//}

使用例

var test=new $js.Xxx

//メソッドとして実行
test.aaa('hello')

//関数として実行
$js.Xxx.aaa(test,'hello') //
今回定義したメソッドの概要
convert()
arguments を Array に変換します。[1,2,[3,4],5] みたいな入れ子の配列も [1,2,3,4,5] と変換します。
index()
指定した値を持つ索引番号を返します。
each()
順次処理。
remove()
指定した値を持つ配列を削除します。
使用例1

メソッドとして実行する方法

//生成
//(['aaa','bbb','ccc','ddd'])でもOK
var arr= new $js.Array('aaa','bbb','ccc','ddd')

//順次処理
arr.each(function(idx){
alert(this[idx]) //aaa → bbb → ccc → ddd
})

//値指定の索引取得
alert(arr.index('ccc')) //2

//値指定の配列削除
//(['aaa','ccc'])でもOK
alert(arr.remove('aaa','ccc')) //bbb,ddd
使用例2

関数として実行する方法

//生成
//(['aaa','bbb','ccc','ddd'])でもOK
var arr= new $js.Array('aaa','bbb','ccc','ddd')

//順次処理
$js.Array.each(arr,function(idx){
alert(this[idx]) //aaa → bbb → ccc → ddd
})

//値指定の索引取得
alert($js.Array.index(arr,'ccc')) //2

//値指定の配列削除
//(['aaa','ccc'])でもOK
alert($js.Array.remove(arr,'aaa','ccc')) //bbb,ddd

感想

汚染を気にせず安心して拡張できるので、必要に応じてメソッドを追加してけば良いかと思います。
そのうち String や Date でも試してみたいと思います。