Under Translation of ECMA-262 3rd Edition を読んでいて見つけた「故意にに汎用的である」と書かれたメソッド群。Arrayと Stringのほとんどのメソッドが該当する。
Firefoxが Array.prototype.methodなどを Array.methodからも参照できるようにする(している)のも 仕様が汎用的で再利用が可能になっているからだろう。
それら汎用的なメソッドを使い回してやるために、その動作を javascriptのコードで表してみる。
function() { var array = []; var array_index = 0; var item = this; var arg_index = 0; do { if(item instanceof Array) { // (1) var item_index = 0; while(item_index !== item.length) { if(item_index in item) { array[array_index++] = item[item_index++]; } else { ++array_index; ++item_index; // (2) } } } else { array[array_index++] = item; } } while(arg_index !== arguments.length && (item = arguments[arg_index++] || true) ); array.length = array_length; return array; }
function(separator) { var length = this.length>>>0; // (1)-1 if(typeof(separator) === "undefined") { separator = ","; } separator = ToString(separator); if(length === 0) { return ""; } var result; var index = 0; var item = this[index++]; item = (item == null) ? "" : ToString(item); result = item; while(index !== length) { // (1)-2 item = this[index++]; item = (item == null) ? "" : ToString(item); result += separator + item; } return result; } // argが数値の場合は厳密には違うかも。 // 以後もたびたび登場する予定。 function ToString(arg) { return ""+arg.valueOf(); }
function() { var length = this.length>>>0; if(length === 0) { this.length = length; // (1)-1 return; // undefined } length -= 1; var result = this[length]; delete this[length]; this.length = length; // (1)-2 return result; }
function() { var length = this.length>>>0; for(var i = 0; i < arguments.length; ++i) { this[length++] = arguments[i]; } this.length = length; // (1) return length; }
function() { var length = this.length>>>0; var mid = Math.floor(length/2); for(var p = 0; p !== mid; ++p) { var q = length-p-1; var P = this[p], Q = this[q]; if(!(q in this)) { delete[p]; } else { this[p] = Q; } if(!(p in this)) { delete[q]; } else { this[q] = P; } } return this; }
sort()については、this.length>>>0 が用いられることと、thisを変更して thisを返すことだけを書いておいて、i, j要素を比較する手順について。
function compare(i, j) { // (A) 存在しない要素を最後尾へ。 if(!(i in this)) { if(!(j in this)) { return +0; // (1) } else { return 1; } } else if(!(j in this)) { return -1; } var I = this[i], J = this[j]; // (B) undefinedな要素を後ろへ。 if(typeof(I) === "undefined") { if(typeof(J) === "undefined") { return +0; // (1) } else { return 1; } } else if(typeof(J) === "undefined") { return -1; } // (C) ユーザー比較関数 if(typeof(comparefn) !== "undefined") { return comparefn(I, J); } // (D) デフォルト比較方法 (文字列化して昇順) I = ToString(I), J = ToSTring(J); if(I < J) { return -1; } else if(I > J) { return 1; } return +0; }
function() { var length = this.length>>>0; if(length === 0) { this.length = length; // (1) return; // undefined } var result = this[0]; // ひとつずつ前へスライド for(var k = 1; k !== length; ++k) { if(k in this) { this[k-1] = this[k]; } else { delete this[k-1]; } } delete this[length-1]; this.length = length-1; return result; }
function() { var length = this.length>>>0; // 要素を引数の数だけ後ろへシフト。 for(var k = length; k !== 0; --k) { var j = k-1; if(j in this) { this[j + arguments.length] = this[j]; } else { delete this[j + arguments.length]; } } // 先頭に引数をコピー for(var k = 0; k !== arguments.length; ++k) { this[k] = arguments[k]; } this.length = length + arguments.length; // (1) return this.length; }
function(start, end) { var array = []; var array_index = 0; var length = this.length>>>0; start = ToInteger(start); start = (start < 0) ? Math.max(length+start, 0) : Math.min(start, length); end = (typeof(end) === "undefined") ? length : ToInteger(end); end = (end < 0) ? Math.max(length+end, 0) : Math.min(end, length); for(var k = start; k < end; ++k) { if(k in this) { array[array_index++] = this[k]; } else { ++array_index; // (1) } } array.length = array_index; return array; } function ToInteger(x) { x = +x; if(isNaN(x)) { return +0; // (2) } if(x === 0 || !isFinite(x)) { return x; } // 0 に近づける。(小数点以下を切り捨てる) return (0 <= x) ? Math.floor(x) : Math.ceil(x); }
function(start, deleteCount) { var array = []; var length = this.length>>>0; start = ToInteger(start); start = (start < 0) ? Math.max(length+start, 0) : Math.min(start, le deleteCount = ToInteger(deleteCount); deleteCount = Math.min(Math.max(deleteCount, 0), length-start); // 取り出す要素をコピー。(まだ削除はしない) for(var k = 0; k !== deleteCount; ++k) { var l = start+k; if(l in this) { array[k] = this[l]; } } // (A) 要素を後ろへずらして空きを作る // (後で上書きされてしまう要素までずらしてない?) if(deleteCount < arguments.length) { for(var k = length-deleteCount; k !== start; --k) { var l = k + deleteCount -1; var m = k + arguments.length -1; if(l in this) { this[m] = this[l]; } else { delete this[m]; } } // (B) 要素を前へ詰めて、空きを新要素と同じ数にする } else if(arguments.length < deleteCount) { for(var k = start; k !== length-deleteCount; ++k) { var l = k + arguments.length; var m = k + deleteCount; if(m in this) { this[l] = this[m]; } else { delete this[l]; } } // (なぜ逆順で削除する?) for(var k = length; k !== length-deleteCount+arguments.length; --k) { delete this[k-1]; } } // 追加する要素で thisを上書き if(arguments.length === deleteCount) { for(var k = 0; k !== arguments.length; ++k) { this[start+k] = arguments[k]; } } this.length = length - deleteCount + arguments.length; return array; }
パターンがわかってきた。
Firefox(JavaScript1.5?)では適用すると Array.prototype.method.call(string) is read-onlyという警告がいくつも出る。
一度の呼び出しでいくつも出ることから read-onlyなのはメソッドではなく、自己破壊的な Array.prototype.method()を適用された stringだと思われる。
警告が出るだけで、戻り値は得られるので pop()で末尾の文字、shift()で先頭の文字が得られる。
unshift()、push()の返す数値は使えない。(その長さを持った文字列は存在しないので)
reverse(), sort()は全く役に立たない。役に立つ値を返さないし、並べ替えには失敗しているから。
slice()は String.prototypeに同名のメソッドがすでに存在する。
splice()がちょっと面白く、splice(startと deleteCount) の引数はちょうど String.prototype.substr(start, length)と対応するが、返ってくるのが substr()->"文字列" なのに対し、splice()->["文","字","列"] となる。
string.split("")の代わりに Array.prototype.splice.call(string, 0)としてみるのはいかが? (警告が出る上に IE7では使えませんが)
Stringではなく、配列のようなオブジェクト(コレクションとか argumentsとか)に適用するのが正解か。
// unknown_objectが配列なら unknown_objectのコピー、 // それ以外なら unknown_objectを唯一の要素とする配列が返る。 var array = Array.prototype.concat.call(unknown_object);
// lengthプロパティと [] での要素アクセスが可能な、 // 配列っぽいオブジェクト(NodeListとか argumentsとか)を配列化。 var array = Array.prototype.slice.call(document.getElementsByTagName("pre"), 0);
// " " が得られます。 var softtab = Array.prototype.join.call({length:8+1}, " "); // softtabを使ったレベル3のインデントが得られます。 var indent = new Array(3+1).join(softtab); // prototypeいらねー
二番目の書き方を使うなら Arrayのビルトインメソッドを他のオブジェクトに使い回すという趣旨が……。
今回から String.prototypeのメソッド。Stringは自己破壊的なメソッドを持っていないから、Arrayのビルトインメソッドと違って適用範囲が広いことを期待している。
……と思ったが、Stringのビルトインメソッドはどれも事前に ToString(this)を呼んでしまう。(ToString()については (1))
indexOfで配列の探索ができるんじゃないかと期待していたが無理だった。がっかり。
これにて終了。
split()に渡す正規表現のサブマッチに特別な意味があるとか、split()はグローバルフラグを無視するとか、グローバルフラグの立った正規表現で exec(string)を呼んで空文字列にマッチしたときはこちらで lastIndexプロパティを 1増やさないといけないとか、もやもやしてたことが Under Translation of ECMA-262 3rd Editionにはいろいろ書いてありました。訳者に感謝。