prototype.jsを読み解く

第2回 Prototypeライブラリ(198~639行目)

この記事を読むのに必要な時間:およそ 19.5 分

Templateクラス

0426: var Template = Class.create();
0427: Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
0428: Template.prototype = {
0429:   initialize: function(template, pattern) {
0430:     this.template = template.toString();
0431:     this.pattern  = pattern || Template.Pattern;
0432:   },
0433: 
0434:   evaluate: function(object) {
0435:     return this.template.gsub(this.pattern, function(match) {
0436:       var before = match[1];
0437:       if (before == '\\') return match[2];
0438:       return before + String.interpret(object[match[3]]);
0439:     });
0440:   }
0441: }
0442: 

Templateクラスです。426行目で通常通りClass.create()を使ってTemplateクラスを作ります。

427行目では,クラス定数として後で使われるTemplate.Patternが定義されています。

429行目からがクラスのコンストラクタです。引数としてtemplate, patternを取ります。evaluate()メソッド実行時のために,インスタンスプロパティに各々保存しておきます。

434行目からが実際にテンプレート置換を行うevaluate()メソッドです。コンストラクタで渡されたtemplate文字列に対して,gsub()を使って置換を行います。その際にデフォルトではTemplate.Patternすなわち/(^|.|\r|\n)(#\{(.*?)\})/という正規表現を置換元とし,435行目からの関数を置換先とします。

ここでTemplate.Patternは,二つのグルーピングがあり,後半で#{foo}という形式の置換対象文字列を,前半でその直前の一文字を捕まえています。直前の一文字が\だった場合には,エスケープされているとして置換を行わず,そうでなければevaluate()に渡されたオブジェクトから値を取得し,置換した結果を返します。

0443: var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead');
0444: 

443行目では,$break,$continueという,実行制御に使う変数を定義しています。

$breakは,Enumerable型のループ内で使われます。$continueは今は使われていません。

Enumerable オブジェクト

0445: var Enumerable = {
0446:   each: function(iterator) {
0447:     var index = 0;
0448:     try {
0449:       this._each(function(value) {
0450:         iterator(value, index++);
0451:       });
0452:     } catch (e) {
0453:       if (e != $break) throw e;
0454:     }
0455:     return this;
0456:   },
0457: 

Enumerableは,Arrayなどの別のクラスにmixinされることが前提で,クラスとしては作られていません。単純にオブジェクト内にメソッドが定義される,という形となっています。

446行目からはeach()メソッドです。派生クラスで定義される_each()メソッドを使って,引数として渡された iterator関数オブジェクトが毎回呼び出されるように関数オブジェクトを作って渡します。ループが何回呼び出されるか,という条件は派生クラス側で定義されます。

この際,各ループを一気に抜けるbreak相当を実現するのに$breakオブジェクトが使われています。イテレータ内でループを抜けたい場合には$breakをthrowし,ここでそれをcatchします。もし$break以外の例外をcatchした場合は,通常の例外として再度throwします。

0458:   eachSlice: function(number, iterator) {
0459:     var index = -number, slices = [], array = this.toArray();
0460:     while ((index += number) < array.length)
0461:       slices.push(array.slice(index, index+number));
0462:     return slices.map(iterator);
0463:   },
0464: 

458行目からはeachSlice()です。indexは配列の分割処理のインデックスですが,while句で(index += number)という比較をしているため,indexは0,number,number * 2,number * 3,…と増えていきます。slicesに分割された配列が格納され,arrayはEnumerableを継承したインスタンスを配列に変換したもので初期化されます。

461行目でnumber個ごとにarrayを分割してslicesにpush()し,最後にslicesに対してmap(iterator)を呼んで返します。map()においてiteratorが指定されていない場合にはPrototype.Kが使われるので,その場合はslicesがそのまま返る形になります。

0465:   all: function(iterator) {
0466:     var result = true;
0467:     this.each(function(value, index) {
0468:       result = result && !!(iterator || Prototype.K)(value, index);
0469:       if (!result) throw $break;
0470:     });
0471:     return result;
0472:   },
0473: 

465行目からはall()メソッドです。

インスタンス中にひとつでも偽と評価されるものがあれば偽を,それ以外なら真を返します。

まずデフォルト値としてresultにtrueをセットします。

その後,インスタンスに対してeach()を適用し,各々の値に対して467行目のリテラル関数オブジェクトを実行します。

468行目において,resultが真かつ評価関数 (iterator || Prototype.K)(value, index) が真なら再度resultに真を代入,そうでなければresultは偽となります。

その後,469 行目でresultが偽ならthrow $breakでループを抜けます。この時,resultは偽となっており,all()の返り値もこの値となります。

全ての要素中で評価関数がひとつも偽とならなかった場合にはresultが真となり,その値を返します。

0474:   any: function(iterator) {
0475:     var result = false;
0476:     this.each(function(value, index) {
0477:       if (result = !!(iterator || Prototype.K)(value, index))
0478:         throw $break;
0479:     });
0480:     return result;
0481:   },
0482: 

474行目からのany()メソッドはall()と逆で,ひとつでも真と評価されればany()は真を返します。

外枠のループはall()とほぼ同様で,ループ中で評価関数が真となるものが見付かるとresultに真をセットしthrow $breakして抜けてany()が真を返す,という形になります。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。