prototype.jsを読み解く

第3回Prototypeライブラリ(640~931行目)

ソースの意図の探り方

第三回目です。

オープンソースの成果物を利用する際に、より理解を深めようとするとソースを追っていく必要がでてきます。その際、ソースの特定の箇所が、なぜそうなっているのかが理解できない部分に遭遇することもよくあります。

幸いなことに、多くのオープンソースの配布物はCVSやSubversionなどのバージョン管理システムで管理されており、その履歴が公開されています。

Prototypeライブラリは現在ではRuby on RailsのSubversionリポジトリ上で管理されており、この場合はRails Tracのリポジトリブラウザから辿っていくのがわかりやすくてお勧めです。

ライブラリを利用する際には単一のprototype.jsというファイルとなっていますが、リポジトリ上ではいくつかのファイルに分類されて管理されており、rakeコマンドで結合してリリースされています。

そこで、まずはバージョンの履歴をさかのぼり、コミットログを確認していく、という所からはじめます。Railsのリポジトリはログも丁寧に書かれており、関連するTrack Ticketの番号も明記されています。

では、今回はArray関連からです。

$A()関数

0640: var $A = Array.from = function(iterable) {
0641:   if (!iterable) return [];
0642:   if (iterable.toArray) {
0643:     return iterable.toArray();
0644:   } else {
0645:     var results = [];
0646:     for (var i = 0, length = iterable.length; i < length; i++)
0647:       results.push(iterable[i]);
0648:     return results;
0649:   }
0650: }
0651: 

640行目からは$A()関数です。同時にArray.form()というエイリアスも作られています。引数として渡されたオブジェクトを、できるだけ配列型にして返そうとします。

641行目では、渡されたものがそもそも偽として判断されるようなものには空配列を返しています。

次に、渡されたiterableオブジェクトがtoArrayプロパティを持っている場合には、それをメソッドとみなして呼び出し、返り値を返します。EnumerableをmixinしているクラスのオブジェクトはここでtoArray()が使われます。

それ以外の場合、645行目からfor文で中身をひとつずつ取り出してresultsにpush()する、という形で取り出していきます。通常の配列はここの処理となりますが、配列に数字プロパティ以外のプロパティ値がセットされていたとしても、ここではコピーされません。

0652: if (Prototype.Browser.WebKit) {
0653:   $A = Array.from = function(iterable) {
0654:     if (!iterable) return [];
0655:     if (!(typeof iterable == 'function' && iterable == '[object NodeList]') &&
0656:       iterable.toArray) {
0657:       return iterable.toArray();
0658:     } else {
0659:       var results = [];
0660:       for (var i = 0, length = iterable.length; i < length; i++)
0661:         results.push(iterable[i]);
0662:       return results;
0663:     }
0664:   }
0665: }
0666: 

652行目からは、$A()関数のApple WebKit対策です。

中身はほぼ同じですが、toArray()を使う条件が異なります。Safariではテキストノードを含むノードリストに対して素の$A()を使うとクラッシュする、という報告があり、条件を満たすオブジェクトの場合はtoArray()を使わずにforループでまわす、としています。

Array オブジェクトへの拡張

0667: Object.extend(Array.prototype, Enumerable);
0668: 

Arrayの拡張をするのに、まずEnumerableで定義されているメソッドをまるごとArray.prototypeにmixinします。

0669: if (!Array.prototype._reverse)
0670:   Array.prototype._reverse = Array.prototype.reverse;
0671: 

後でArray.prototype.reverse()メソッドを上書きしてしまうので、ここでオリジナルの関数オブジェクトを_reverseに保存しておきます。

0672: Object.extend(Array.prototype, {
0673:   _each: function(iterator) {
0674:     for (var i = 0, length = this.length; i < length; i++)
0675:       iterator(this[i]);
0676:   },
0677: 

mixinしたEnumerable.each()から参照される_each()メソッドです。ここでは単純な配列なので、for文を使って0から順に辿ってiterator関数を呼び出す実装となっています。

0678:   clear: function() {
0679:     this.length = 0;
0680:     return this;
0681:   },
0682: 

678行目からはclear()メソッドです。配列の場合はlengthプロパティに0をセットするだけで要素をクリアできるので、そのようにしています。

0683:   first: function() {
0684:     return this[0];
0685:   },
0686: 
0687:   last: function() {
0688:     return this[this.length - 1];
0689:   },
0690: 

683行目からはfirst()メソッドです。配列では単にthis[0]と先頭の要素を返しています。

687行目からのlast()メソッドも簡単で、this[this.length - 1]が最後の要素なのでそれを返します。

0691:   compact: function() {
0692:     return this.select(function(value) {
0693:       return value != null;
0694:     });
0695:   },
0696: 

691行目からはcompact()メソッドです。nullかundefinedな要素を取り除いた配列を返します。

undefinedを!=でnullと比較すると真になるため(undefinedがnullに暗黙的に型変換される⁠⁠、select()に渡す関数内の判別式はvalue != nullという形となります。

0697:   flatten: function() {
0698:     return this.inject([], function(array, value) {
0699:       return array.concat(value && value.constructor == Array ?
0700:         value.flatten() : [value]);
0701:     });
0702:   },
0703: 

697行目からはflatten()メソッドです。多層構造の配列をフラットな一次元配列に変換します。階層構造に対応するために、再帰的にflatten()を呼び出すようになっています。

689行目で、空配列を初期値としてinject()を呼び出します。呼び出される関数オブジェクトの中では、Arrayの要素値が真で、かつconstructorプロパティがArrayなら再帰的にflatten()を呼び出してその返り値を、そうでなければその値を配列にしてarray.concat()に渡しています。

new式で生成されたオブジェクトは、prototype.constructorとして生成に用いられた関数オブジェクトを持ちます。ここでは、valueとして渡されたオブジェクトのconstructorプロパティが、Arrayのコンストラクタ関数と等価であるかどうかを見ています。

0704:   without: function() {
0705:     var values = $A(arguments);
0706:     return this.select(function(value) {
0707:       return !values.include(value);
0708:     });
0709:   },
0710: 

704行目からはwithout()メソッドです。まず、可変長の引数として渡されているargumentsをvaluesに配列としてまとめます。

706行目でselect()を使って、該当する要素だけを取り出して返します。その際に、関数オブジェクトとして渡している条件判断では、values.include (value)として、Arrayの要素値valueが、values配列の中に含まれているかどうかをチェックしbooleanで返しています(include()では==で比較しています⁠⁠。

この結果、Arrayインスタンスの要素値のうち、valuesに含まれないもののみが配列として返される形になります。

0711:   indexOf: function(object) {
0712:     for (var i = 0, length = this.length; i < length; i++)
0713:       if (this[i] == object) return i;
0714:     return -1;
0715:   },
0716: 

711行目からはindexOf()メソッドです。渡されたobjectが、Array要素内に存在すればそのインデックス値を返します。

712行目からのシンプルなfor文での実装となっています。もし見つからなければ714行目で-1を返しています。

0717:   reverse: function(inline) {
0718:     return (inline !== false ? this : this.toArray())._reverse();
0719:   },
0720: 

717行目からはreverse()メソッドです。これは、元々存在するArray.reverse()を置き換えています。

引数を指定しない場合はArrayインスタンスの中身を直接逆順に並べ替えますが、false を指定すると、インスタンスのコピーを作成し、それを逆順にして返すようになっています。

_reverse()にオリジナルのArray.reverse()関数が保存されているので、処理対象をinline引数を見てthisにするかthis.toArray()でコピーしたものにするか、を変えています。

0721:   reduce: function() {
0722:     return this.length > 1 ? this : this[0];
0723:   },
0724: 

721行目からはreduce()メソッドです。個人的にはあまり用途がわからない関数ですが、lengthプロパティを調べて要素数が1より大きければ配列のまま返し、それ以下なら先頭の要素を(配列を解いて)返しています。

要素数が0の場合もthis[0]として返されるので、その場合はundefinedが返されます。

0725:   uniq: function(sorted) {
0726:     return this.inject([], function(array, value, index) {
0727:       if (0 == index || (sorted ? array.last() != value : !array.include(value)))
0728:         array.push(value);
0729:       return array;
0730:     });
0731:   },
0732: 

725行目からはuniq()メソッドです。inject()を空配列を初期値として呼び出して、その結果を返しています。

呼び出されるイテレータ関数の中では、配列の先頭の場合(index == 0)にはそのままarrayにpush()します。それ以外の場合はuniq()に渡されるsortedフラグに依存して、sortedが真の場合、直前にarrayにpush()したものと異なっていれば今回の値もpush()する、で済みます。sortedが偽の場合、array.include()を呼び出して過去にpush()したarrayの中に今回の値が含まれているかどうかをチェックして、存在しなければpush()する、とします。

最後のinclude()を使ったチェックが、長い配列だと遅くなりうる処理になるので注意しましょう。ソートされた配列を使ってsortedフラグを真にすれば、include()を使わずに済みます。

0733:   clone: function() {
0734:     return [].concat(this);
0735:   },
0736: 

733行目からはclone()メソッドです。配列をコピーする方法はいろいろとあると思いますが、ここでは空配列に対してArray.concat()を使うことでコピーを作成しています。

0737:   size: function() {
0738:     return this.length;
0739:   },
0740: 

737行目からはsize()メソッドです。Arrayなので単純にlengthプロパティを返しています。

0741:   inspect: function() {
0742:     return '[' + this.map(Object.inspect).join(', ') + ']';
0743:   },
0744: 

741行目からはArrayのinspect()メソッドです。

JSON 表記と同じように(というのも変ですね。リテラルな配列オブジェクト表記、でしょうか⁠⁠、'[', ']' で要素を囲んで出力しています。

各要素の出力はObject.inspect()に任せて、各々が適切な文字列表記を返してもらいます。返ってきた配列をjoin(', ')で連結して終了です。

0745:   toJSON: function() {
0746:     var results = [];
0747:     this.each(function(object) {
0748:       var value = Object.toJSON(object);
0749:       if (value !== undefined) results.push(value);
0750:     });
0751:     return '[' + results.join(', ') + ']';
0752:   }
0753: });
0754: 

745行目からはArray用のtoJSON()メソッドです。

格納場所として配列resultsを空で用意して、Array インスタンスに対してeach()ループを実行します。

イテレータ関数の内部では、Object.toJSON()を呼び出し、返り値がundefinedでない時のみresultsにpush()しています。Object.toJSON()からundefinedが返るのは、要素がundefined, 関数の時などです。

0755: Array.prototype.toArray = Array.prototype.clone;
0756: 

755行目で、Array.clone()の別名としてArray.toArray()を用意しています。

0757: function $w(string) {
0758:   string = string.strip();
0759:   return string ? string.split(/\s+/) : [];
0760: }
0761: 

757行目からは$w()関数です。

まずString.strip()で前後の空白(\s)を取り除き、stringが空でなければsplit(/\s+/)したものを、空なら[]で空配列を返します。

0762: if (Prototype.Browser.Opera){
0763:   Array.prototype.concat = function() {
0764:     var array = [];
0765:     for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
0766:     for (var i = 0, length = arguments.length; i < length; i++) {
0767:       if (arguments[i].constructor == Array) {
0768:         for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
0769:           array.push(arguments[i][j]);
0770:       } else {
0771:         array.push(arguments[i]);
0772:       }
0773:     }
0774:     return array;
0775:   }
0776: }

762行目からは、Opera用にArray.concat()関数を再定義しています。

Operaがネイティブに実装しているArray.concat()が正しく動かないので、上書きするということで、Revision 5550で追加されています。

どのバージョンのOperaのネイティブの実装に問題があるのか、どういう問題なのか、は未調査です。

Hashオブジェクト

0777: var Hash = function(object) {
0778:   if (object instanceof Hash) this.merge(object);
0779:   else Object.extend(this, object || {});
0780: };
0781: 

777行目からはHashのコンストラクタです。

778行目では、コンストラクタの引数としてHashインスタンスが渡された時に、初期値としてマージします。もしHash以外のオブジェクトが渡された場合は、Object.extend()を使ってプロパティごとにコピーをして初期設定を行います。

0782: Object.extend(Hash, {
0783:   toQueryString: function(obj) {
0784:     var parts = [];
0785:     parts.add = arguments.callee.addPair;
0786: 
0787:     this.prototype._each.call(obj, function(pair) {
0788:       if (!pair.key) return;
0789:       var value = pair.value;
0790: 
0791:       if (value && typeof value == 'object') {
0792:         if (value.constructor == Array) value.each(function(value) {
0793:           parts.add(pair.key, value);
0794:         });
0795:         return;
0796:       }
0797:       parts.add(pair.key, value);
0798:     });
0799: 
0800:     return parts.join('&');
0801:   },
0802: 

783行目からはtoQueryString()メソッドです。

ここではまだHash.toQueryStringとして定義しており、Hash.prototype.toQueryStringはもう少し後ろで別途定義しています。

まず784行目で空の配列を用意し、そのaddプロパティとしてHash.toQueryString.addPair関数オブジェクトを代入しています(基本的には二つの引数を'='でjoin()する関数です⁠⁠。これで、parts.add()が呼び出されたときに、その関数内でのthisはpartsを示すことになります。

787行目では、後ろで定義されるHash.prototype._each()を、objがthisになるような形で呼び出しています。_each()内では{ key:'...', value:'...' }となるオブジェクトが生成され、引数としてイテレータ関数function(pair)に渡されます。

788行目からのイテレータ関数はtoQueryString()に渡したオブジェクトobjのプロパティごとに呼び出されます。オブジェクトのプロパティ値がオブジェクトではなく単なる値の場合、797行目でpartsにadd()されます。

objのプロパティ値が配列だった場合には、その中身が同じプロパティ名で連続してparts.add()されます。たとえばobjが{ a:[1,2,3] }の場合にはtoQueryString()はa=1&a=2&a=3を返します。

_each()のループが終わると、最後にpartsをjoin('&')して繋げて返します。

0803:   toJSON: function(object) {
0804:     var results = [];
0805:     this.prototype._each.call(object, function(pair) {
0806:       var value = Object.toJSON(pair.value);
0807:       if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value);
0808:     });
0809:     return '{' + results.join(', ') + '}';
0810:   }
0811: });
0812: 

803行目からはHash.toJSON()です。これもHash.prototype.toJSON()はまた別途後ろで定義されます。

toQueryString()と同じように、Hash.prototype._each()を呼び出して、各々のプロパティについて処理しています。

value がundefinedの場合は処理しないようにし、そうでなければプロパティ名、プロパティ値共にtoJSON()を呼び出して、': 'で結合しています。ここで、プロパティ名は単なる文字列ですが、値の方は複雑なオブジェクトになりうるので、多くの場合に対応できるObject.toJSON()を使っています。

後は809行目で、'{', '}'で括った文字列として返して終了です。

0813: Hash.toQueryString.addPair = function(key, value, prefix) {
0814:   key = encodeURIComponent(key);
0815:   if (value === undefined) this.push(key);
0816:   else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
0817: }
0818: 

先ほど使ったHash.toQueryString.addPair()関数です。

thisは配列であることを前提とし、key, valueをURIエンコードして'='で繋げた文字列として返します。

815行目にあるように、値がundefinedの場合はキー名だけを文字列として返しています。値がnullの場合は'キー名='と値が空で返ります。

0819: Object.extend(Hash.prototype, Enumerable);
0820: Object.extend(Hash.prototype, {

まずHash.prototypeにEnumerableで定義されたメソッドをまるごとmixinします。その後、Hash独自のメソッドの定義に入ります。

0821:   _each: function(iterator) {
0822:     for (var key in this) {
0823:       var value = this[key];
0824:       if (value && value == Hash.prototype[key]) continue;
0825: 
0826:       var pair = [key, value];
0827:       pair.key = key;
0828:       pair.value = value;
0829:       iterator(pair);
0830:     }
0831:   },
0832: 

まずは_each()メソッドです。Enumerable.each()から呼ばれます。

for (var value in object)構文を使って、インスタンス内のDontEnum属性が付いていないプロパティを列挙していきます。

824行目では、プロパティ値がHash.prototype[key]と同一かどうかを確認しています。これは、スクリプト内でHashに追加したメソッド用のプロパティなどは、DontEnum属性が付いていないためにfor (var ... in ...)で列挙されてしまいます。prototype以下に定義してある型のテンプレートとしてのプロパティは、通常列挙されて欲しくない場合が多いので、prototypeから引き継いだものはスキップされるようにしています。

Hashの場合、prototypeプロパティ以下にはライブラリにより追加されたメソッドが多く含まれています。一方、Hashインスタンス直下には、Hashの用途であるデータの保存を目的として、様々な名前を持ったプロパティが追加されていきます。この際、列挙されるのは自分でインスタンスに追加したデータのみ、という条件が好ましいので、このようにprototype以下との比較チェックが入っています。

あとは826行目からHashのeach()のイテレータ関数で使われるpairオブジェクトを構築しています。これは[ key, value ]の配列として作りつつ、"key", "value"というプロパティも持つ、というオブジェクトになっています。

これを伴いイテレータ関数を呼び出して終了です。

0833:   keys: function() {
0834:     return this.pluck('key');
0835:   },
0836: 
0837:   values: function() {
0838:     return this.pluck('value');
0839:   },
0840: 

833行目からはkeys()メソッドです。Enumerable.pluck()を利用し、Hash.prototype._each()でpairオブジェクトの'key'プロパティの値を配列として返すようになっています。

837行目からのvalues()メソッドも同様で、pairオブジェクトの'value'プロパティを参照しているだけの違いです。

0841:   merge: function(hash) {
0842:     return $H(hash).inject(this, function(mergedHash, pair) {
0843:       mergedHash[pair.key] = pair.value;
0844:       return mergedHash;
0845:     });
0846:   },
0847: 

841行目からはmerge()メソッドです。

$H()で引数で渡されたオブジェクトをHash化し、thisを初期値としてinject()を呼び出します。

あとはイテレータ関数に来たpairオブジェクトを使って、mergedHashにプロパティを追加していき、最終的にinject()から返ってきたHashを返します。

0848:   remove: function() {
0849:     var result;
0850:     for(var i = 0, length = arguments.length; i < length; i++) {
0851:       var value = this[arguments[i]];
0852:       if (value !== undefined){
0853:         if (result === undefined) result = value;
0854:         else {
0855:           if (result.constructor != Array) result = [result];
0856:           result.push(value)
0857:         }
0858:       }
0859:       delete this[arguments[i]];
0860:     }
0861:     return result;
0862:   },
0863: 

848行目からはremove()メソッドです。

この関数は取り除いたプロパティの値を返すので、その保存用にresult変数を定義します。remove()の引数は可変長のため、arguments変数をfor文でループして、ひとつずつ処理します。

まず、851行目でHashインスタンスからプロパティ値を取り出そうとします。値がundefinedなら、そのHashには指定されたプロパティが存在しなかった、ということでスキップします。

853行目では、まだresultを使っていない時の処理で、単に値を代入します。もし2回目以降の場合、resultがまだ配列でなければ1要素の配列に変換します。その後、今回の値をpush()します。resultは返す値がひとつしかなければそのまま、複数なら配列として返されるようになっているのでこういう処理となっています。

あとは指定されたプロパティをdeleteで消し、ループが終わるとresultに入った値を返します。

0864:   toQueryString: function() {
0865:     return Hash.toQueryString(this);
0866:   },
0867: 

864行目のtoQueryString()メソッドは、Hash.toQueryString()で定義した関数を呼ぶだけです。

Hash.toQueryString()関数は、インスタンスのメソッドとして以外でも使えるように、グローバルはHashオブジェクト直下に置かれています。インスタンスから呼ばれる時は、このメソッドを経由して呼ばれる形になります。

0868:   inspect: function() {
0869:     return '#<Hash:{' + this.map(function(pair) {
0870:       return pair.map(Object.inspect).join(': ');
0871:     }).join(', ') + '}>';
0872:   },
0873: 

868行目からはHash版のinspect()メソッドです。

'#<Hash:{...}>'という表記でHashであることがわかるように文字列化し、中身は万能Object.inspect()を使って出力します。

0874:   toJSON: function() {
0875:     return Hash.toJSON(this);
0876:   }
0877: });
0878: 

874行目からはtoJSON()メソッドです。これもHash.toJSON()をそのまま呼んでいるだけです。

0879: function $H(object) {
0880:   if (object instanceof Hash) return object;
0881:   return new Hash(object);
0882: };
0883: 

879行目からは$H()関数です。

渡されたオブジェクトがすでにHashインスタンスならそのまま返し、そうでなければnew Hash()を使ってHashインスタンスとします。これで$()関数と同様に、⁠渡すものがHashかどうかわからないけどとにかくHashにしてくれ」という形で使うことができるので便利です。

0884: // Safari iterates over shadowed properties
0885: if (function() {
0886:   var i = 0, Test = function(value) { this.key = value };
0887:   Test.prototype.key = 'foo';
0888:   for (var property in new Test('bar')) i++;
0889:   return i > 1;
0890: }()) Hash.prototype._each = function(iterator) {
0891:   var cache = [];
0892:   for (var key in this) {
0893:     var value = this[key];
0894:     if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
0895:     cache.push(key);
0896:     var pair = [key, value];
0897:     pair.key = key;
0898:     pair.value = value;
0899:     iterator(pair);
0900:   }
0901: };

884行目からは、Safari用にHash.prototype._each()を入れ替えています。

まず、885行目からのfunction(){ ... }()という文は、名前空間を汚さずにローカル変数を使った処理を書くときの定型句です。リテラルな関数オブジェクトを作って、それを()演算子で呼び出しています。関数内でvarで定義した変数はその外のスコープからは見えません。

886行目からの処理で判別しているのは、SafariがFoo.prototype.barとFoo.barというプロパティが存在するときに、barを二回列挙してしまう、という問題があるためです。

ここではブラウザのUserAgentを使った判別は行わず、実際に試してみて判別しています。

890行目からの、別バージョンのHash.prototype._eachは、821行目からのオリジナルとあまり変わらず、列挙されるプロパティ名をcache変数に貯めておき、同じプロパティ名が再度出てきたらスキップする、という処理を加えただけです。

ObjectRangeクラス

0902: ObjectRange = Class.create();
0903: Object.extend(ObjectRange.prototype, Enumerable);

902行目からはObjectRangeクラスです。

まずはClass.create()で雛形をつくり、EnumerableをObjectRange.prototypeにmixinしています。

0904: Object.extend(ObjectRange.prototype, {
0905:   initialize: function(start, end, exclusive) {
0906:     this.start = start;
0907:     this.end = end;
0908:     this.exclusive = exclusive;
0909:   },
0910: 

コンストラクタでは、単にインスタンス変数に引数を保存しているだけです。また、このコンストラクタは後述する$Rショートカット関数と同じインターフェイスを持っています。

0911:   _each: function(iterator) {
0912:     var value = this.start;
0913:     while (this.include(value)) {
0914:       iterator(value);
0915:       value = value.succ();
0916:     }
0917:   },
0918: 

Enumerable.each()から呼び出される_each()メソッドです。

ObjectRangeオブジェクトは、範囲が大きかろうとすべての値を配列で持っているわけではなく、先頭と終了、境界値を含むかどうかのフラグ、しか持っていません。

そのため、_each()内のwhileループでは、this.startから開始して、それが範囲内に含まれているかどうかをthis.include()でチェックして、含まれていればイテレータ関数を呼び出して次の値をthis.succ()で取得する、という形になっています。

0919:   include: function(value) {
0920:     if (value < this.start)
0921:       return false;
0922:     if (this.exclusive)
0923:       return value < this.end;
0924:     return value <= this.end;
0925:   }
0926: });
0927: 

919行目からはinclude()メソッドです。

最初はthis.start未満だったら偽を返します。

this.exclusiveが真の場合、this.endが示す値はObjectRangeの範囲に含まれません。そのため 923行目では渡された値がthis.end未満なら真、そうでなければ偽を返しています。

this.exclusiveが偽の場合、this.end以下なら真、そうでなければ偽を返します。

0928: var $R = function(start, end, exclusive) {
0929:   return new ObjectRange(start, end, exclusive);
0930: }
0931: 

928行目からは$R()ショートカット関数です。

単にnewでObjectRangeインスタンスを作成し、返しているだけです。

おすすめ記事

記事・ニュース一覧