jQuery的なarray like objectの作り方

twitterはじめました。 agoです。

前回の記事(jQueryを読むために知っておきたい6つの知識)で「jQueryのthisはArray」と書いたところ、「正しくはArray likeではないか?」とのご指摘を頂きその点調査したのでご紹介したいと思います。

まず、通常javascriptのobjectはkey(property名)に文字列以外設定できず、それ以外の変数(object)を指定した場合、.toString()を呼び出して文字列化した結果で保存されます。 (ただし、{'key':'val'}といったobjectリテラルの場合、key(property名)に記述された内容はそのまま文字列と解釈されます)

function huga () {}
var hoge = {};
hoge[huga] = 1;
for (var i in hoge) { console.log(typeof i, i); };
//=> string function huga() { }

ここまではobjectをhash(連想配列)として使う場合の通常の動作ですが、jQueryではご存じのようにobjectをArrayのように連結した値として持っています。

console.log($('a')[0]);
// => <a href="/">

当初は違和感があったのですが、そのうち「Arrayでも継承してるのかな?」と思う程度で特に疑問も感じなくなりました。

そこで、前回「thisがArray」と書いたところ、twitter経由で「正確にはArray like」とのご指摘を頂き、詳しく調べてみたところ、以下のような記述を発見しました。 (以前jQueryのソースを読んだときに見ていたはずなんですが、気づきませんでした。。。)

142:    setArray: function( elems ) {
143:        // Resetting the length to 0, then using the native Array push
144:        // is a super-fast way to populate an object with array-like properties
145:        this.length = 0;
146:        Array.prototype.push.apply( this, elems );
147:        
148:        return this;
149:    },

コメントにも「.lengthに0をセットして、Array.prototype.pushを呼び出すと高速にarray likeなobjectが作れる」と記述されていますが、これはつまり次のような動作になるようです。

var hoge = {'length' : 1};
console.log(hoge[0]);
//=> undefined
Array.prototype.push.call(hoge, 1);
console.log(hoge[0]);
//=> 1

ただ、この場合でも、hoge自体が変化したわけではないので、typeofや.toString()はobjectと判断されます。

これに関してもう少し詳しく調べてみました。

//先に要素が存在する場合もちゃんと最後に追加される
var hoge = {'length' : 1, 0:1};
Array.prototype.push.call(hoge, 2);
console.log(hoge[1]);
//=> 2

//lengthがない場合、数値キーの内容は破棄される
var hoge = {0:1, hoge:1};
Array.prototype.push.call(hoge, 2);
console.log(hoge);
//=> { 0:2, length:1, hoge:1 }

//後付でもlengthを定義すれば値は保持される
var hoge = {0:1, 1:2};
Array.prototype.push.call(hoge, 9);
console.log(hoge);
//=> { 0:9, 1:2, length:1 }
hoge.length = 2
Array.prototype.push.call(hoge, 9);
console.log(hoge);
//=> { 0:9, 1:2, length:1, 1:9 }

この方法の利点としては、継承可能で他objectの.prototypeを汚さず、Arrayのように扱えるobjectを簡単に定義できることがあげられます。 また、欠点としては(主観ですが)あまり一般的な手法でないことと、methodの呼び出しが長くなること(Array.prototype.hoge.call(obj))があります。

ちなみに、普通にArrayを継承するのであれば以下のような方法で定義することも可能です。

var hoge = function () {}; hoge.prototype = new Array(); hoge.prototype.gege = 1; var huga = new hoge(); console.log(huga.gege); // => 1 console.log((new Array).gege); // => undefined

ただし、こちらの方法ではArray全体を継承するため、jQuery的な方法に比べて重くなる欠点があります。 (10/6 追記 ブックマーク経由でArrayの継承はちょっと問題がという指摘を受けたので削除しました。ご指摘ありがとうございます)

あまり実用的な知識ではありませんが、ちょっと面白い内容だったのでまとめてみました。

カヤックでは多少乗り遅れても追いかける技術者も募集しています!

関連記事