prototype.jsを読み解く

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

ECMA-262 第三版について

第二回目です。

本連載中には、参考資料としてリンクできるところはリンクとして掲載するようにしています。しかし、もっとも重要であろうECMA-262 第三版については、PDFがあるだけなのでセクション番号とタイトルだけが記載されています。

仕様書としてはわかりにくい部類に入るかな、とも思いますが、JavaScriptの挙動を理解するには最終的にはこの文書を参照せざるを得ません。できればダウンロードしていつでも参照できるようにしておくといいでしょう。

また、この長大な仕様を翻訳してくださっている方も存在します。わかりにくい仕様なので、細かいニュアンスを押さえるには原文を参照するしかありませんが、概要を掴むには便利かと思います。

では、今回はStringオブジェクトから見ていきましょう。

String.interpret() と String.specialChar

0198: Object.extend(String, {
0199:   interpret: function(value) {
0200:     return value == null ? '' : String(value);
0201:   },
0202:   specialChar: {
0203:     '\b': '\\b',
0204:     '\t': '\\t',
0205:     '\n': '\\n',
0206:     '\f': '\\f',
0207:     '\r': '\\r',
0208:     '\\': '\\\\'
0209:   }
0210: });
0211: 

別の箇所で使うために、静的関数と定数値を定義します。

ここではprototypeプロパティではなくStringオブジェクト直下に直接追加しているので、呼び出すときはStringのインスタンス経由ではなく、String.interpret()やString.specialCharのように参照しないといけません。

199行目のString.interpret()では、引数として渡された値がnullなら空文字列を返し、それ以外ならそのままStringオブジェクトにして返す、ということをしています。

これにより、値がnullかどうかを気にせずに、文字列連結演算ができるようになります。

例:
result += String.interpret(replacement(match));

specialCharには特殊文字列と、それをエスケープした表現の対応が入っています。例えばString.specialChar['\n']とすると'\\n'(最初がバックスラッシュ、次がASCIIの'n'という2バイト)が返ります。

String.prototype.inspect()メソッドで、人間向けの文字列表現に変換する際に使われています。

String オブジェクトへの拡張

String.prototypeに対しては、数多くの拡張が入っています。これらは全てのStringオブジェクトで使えるメソッドとなります。

0212: Object.extend(String.prototype, {
0213:   gsub: function(pattern, replacement) {
0214:     var result = '', source = this, match;
0215:     replacement = arguments.callee.prepareReplacement(replacement);
0216: 
0217:     while (source.length > 0) {
0218:       if (match = source.match(pattern)) {
0219:         result += source.slice(0, match.index);
0220:         result += String.interpret(replacement(match));
0221:         source  = source.slice(match.index + match[0].length);
0222:       } else {
0223:         result += source, source = '';
0224:       }
0225:     }
0226:     return result;
0227:   },
0228: 

まずは213行目からの gsub()メソッドです。基本的にはpatternにマッチする文字列を見つけたら、slice()でマッチ位置より前の文字列をresultに足し(219行目⁠⁠、マッチ部分を置換した結果をresultに足し(220行目⁠⁠、ループ用のsource文字列をマッチ以後の文字列に更新する(221行目)ということを繰り返しています。

215行目でreplacementを関数オブジェクトにしている所が多少わかりにくいかもしれません。

ここで、argumentsはJavaScriptが自動的に用意してくれるオブジェクトで、関数に入るときに作成されます。そのarguments.calleeは今回呼び出された関数自身を示すFunctionオブジェクトが入ります(この場合gsub()関数⁠⁠。String.prototype.gsub.prepareReplacement()という関数は、別途411行目で定義されていて、replacement引数として

  • 単純な置換後の文字列
  • RegExp.exec()の返す配列を引数として受け取り、受け取り置換後の文字列を返すFunctionオブジェクト
  • Prototypeライブラリが提供するTemplateクラスのオブジェクト

のどれが渡されたとしても、⁠RegExp.exec()の返す配列を引数として受け取り、置換後の文字列を返す」というFunctionオブジェクトに統一して返すようになっています。

……という挙動を意図しているのだと思われますが、実際の動作としては、replacemntとしてFunctionオブジェクトを渡していない場合には、必ずTemplateクラスのコンストラクタに渡されて、テンプレート文字列として処理されてしまいます。なので、Templateの特殊文字である#{...}という書式をreplacementに書いてしまうと、Template.evaluate()が処理してしまい意図と異なる結果となってしまうかもしれません。

例えば以下のようになります。

var s = "ABC";
var output = s.gsub('ABC', 'Template では #{...} と書きます。');
alert(output);    # 'Template では  と書きます。' が出力される
0229:   sub: function(pattern, replacement, count) {
0230:     replacement = this.gsub.prepareReplacement(replacement);
0231:     count = count === undefined ? 1 : count;
0232: 
0233:     return this.gsub(pattern, function(match) {
0234:       if (--count < 0) return match[0];
0235:       return replacement(match);
0236:     });
0237:   },
0238: 

String.prototype.gsub()がグローバル置換(マッチするもの全てに対して置換する)だったのに対して、sub()では最大指定された回数まで置換を行います。

233行目ではgsub()のreplacement引数にFuncitonオブジェクトを渡す方法を用いて、指定された回数を越えていたら置換を行わず、そうでなければ普通に置換する、という処理を行う関数を渡しています。

0239:   scan: function(pattern, iterator) {
0240:     this.gsub(pattern, iterator);
0241:     return this;
0242:   },
0243: 
0244:   truncate: function(length, truncation) {
0245:     length = length || 30;
0246:     truncation = truncation === undefined ? '...' : truncation;
0247:     return this.length > length ?
0248:       this.slice(0, length - truncation.length) + truncation : this;
0249:   },
0250: 

scan()はgsub()を呼び出しているだけですが、gsub()が置換後の文字列を返すのに対して、scan()は元の文字列をそのまま返します。パターンにマッチした文字列に対して指定の処理を行いたい、という場合に使えます。

truncate()では、lengthが指定されていなかった場合に30という値を設定し、truncationが指定されていない場合は文字列'...'を設定します。

Stringオブジェクトでは内部がUnicodeなので、文字列の長さを指定するときも日本語も1文字として数えます。

0251:   strip: function() {
0252:     return this.replace(/^\s+/, '').replace(/\s+$/, '');
0253:   },
0254: 
0255:   stripTags: function() {
0256:     return this.replace(/<\/?[^>]+>/gi, '');
0257:   },
0258: 
0259:   stripScripts: function() {
0260:     return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
0261:   },
0262: 

strip()では、文字列の前後の空白を除去しています。String.replace()が置換後の Stringオブジェクトを返すので、先頭の空白を除去するreplace()、最後の空白を除去するreplace()と連続処理させて返しています。

stripTags()では「HTMLタグのようなもの」を除去しています。この正規表現は

< <自身
\/? あってもなくてもいい / 文字 (これで開くタグ、閉じるタグをカバー)
[^>]+ タグ終端となる > 文字以外が1文字以上連続したもの
> >自身
gi g は置換をグローバルに、i は大文字小文字を無視(これは不要なはず)

という内容になっていて、HTMLタグのようなものを空文字列に置換します。

stripScripts()では、Prototype.ScriptFragment(すなわち'<script[^>]*>([\\S\\s]*?)<\/script>'という文字列)を使って<script>タグとその中身をごっそりと除去します。正規表現の中身は

<script[^>]*> <script>タグ(属性があっても可)
([\\S\\s]*?) [\\S\\s]で任意の文字(.では改行がマッチしない⁠⁠。それに*が付いて0文字以上の任意の文字。最後に?が付くので欲張らないマッチとなる。あとはその周囲をextractScripts()などで使うためにグループ化
<\/script> 要素を閉じるための<\/script>タグ

となっています。

0263:   extractScripts: function() {
0264:     var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
0265:     var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
0266:     return (this.match(matchAll) || []).map(function(scriptTag) {
0267:       return (scriptTag.match(matchOne) || ['', ''])[1];
0268:     });
0269:   },
0270: 
0271:   evalScripts: function() {
0272:     return this.extractScripts().map(function(script) { return eval(script) });
0273:   },
0274: 

extractScript()ではPrototype.ScriptFragment正規表現を使って、Stringオブジェクト内の<script>タグを見つけて、そのタグの内側を配列で返します。

まず、matchAll、matchOneという正規表現オブジェクトを作成します。

これらは同じ正規表現ルールを用い、フラグgが付いているかどうかだけが異なります。

gが付いているmatchAllをString.match()メソッドに使うと、複数回マッチしたものが配列として返り、gが付いていないmatchOneは「インデックス0がマッチした文字列全体、インデックス1以降がグループ化部分にマッチした部分文字列」という配列が返ります。

そして、 (this.match(matchAll) || []) で、thisに含まれる全ての<script>タグ部分を配列で返します。もしString.match()がマッチしなかった場合、nullを返すのでその場合は||以降の[]が値となります。

次に、その配列を.map(function(s){ return (s.match(matchOne) || ['',''])[1] })に渡します。map()はEnumerable.map()として定義されているもので、後述します。

ここで、map()の対象となる配列の要素各々には<script>要素がまるごと文字列で入っています。

その各々に対して、今度はイテレータ関数の中でmatchOneでRegExp.global = falseでマッチさせています。

すると、Prototype.ScriptFragmentに含まれるグループ化の()により、一つ目の要素がマッチ文字列全体(この場合は<script>要素全体⁠⁠、二つ目の要素がグループ化の一つ目が入った配列が返されます。

match()が失敗した場合は['','']という配列を返すようにして、そこに対して (配列)[1] という演算を行い二番目の要素だけを取り出し、それをreturnで返しています。

これをextractScript()の呼出し側に返すことで、最終的に「タグの内側の文字列を配列で返す」というこの関数の目的となる処理が達成されます。

271行目からのevalScript()では、上記のextractScript()を呼び出した上で、返ってきた配列に対してmap()で各々をeval()する、という処理を行っています。

これにより、文字列インスタンス内に含まれる<script>タグそれぞれに対し、中身をeval()する、ということになります。

0275:   escapeHTML: function() {
0276:     var self = arguments.callee;
0277:     self.text.data = this;
0278:     return self.div.innerHTML;
0279:   },
0280: 
0281:   unescapeHTML: function() {
0282:     var div = document.createElement('div');
0283:     div.innerHTML = this.stripTags();
0284:     return div.childNodes[0] ? (div.childNodes.length > 1 ?
0285:       $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
0286:       div.childNodes[0].nodeValue) : '';
0287:   },
0288: 

275行目からのescapeHTML()では、&、<、>などの文字を、&amp;、&lt;、&gt;という形でエスケープする処理を行います。

ここでは単純にreplace()などを使うのではなく、空のdiv要素の内側にテキストノードを作成し、そこに該当文字列を代入し、divのinnerHTMLの値を返す、ということでブラウザ側にエスケープ処理を任せるようになっています。

276行目のvar selfはarguments.calleeが自分自身を指す関数オブジェクトなので、escapeHTML自身が入ります。

別途もう少し後の419行目あたりにおいて、String.prototype.escapeHTML.divに空のdocument.createElement('div')を、String.prototype.escapeHTML.textにdocument.createTextNode('')を用意しています。これを424行目のwith (String.prototype.escapeHTML) div.appendChild(text);によって、divの内側にテキストノードが入るように準備します。String.escapeHTML()の呼出しでは、ここで予め準備されたdiv, テキストノードを何度も再利用しています。

これにより、長い文字列でもそれほど速度を落とすことなく、よく使われるであろう置換処理を行うことができるようになっています。

ただし、402行目において、WebKitかIEの場合には上記の処理は行わずに、String.replace()を使った地味な置換処理をするような形でescapeHTML()関数自体を置き換えています。そのため、上記の処理が行われるのはそれ以外のブラウザー、ということになります。

これは、IEではテキストノードに含まれる\nがinnerHTMLを経由すると消えてしまう、ということへの対処のためです。WebKit(Safari)は単に最適化のためにこちらの関数を使っているようです。

このメソッドescapeHTML()は、該当文字に対してのみ置換が行われる、という想定なので、空白文字であろうとも消えてしまう、というのは困るのでそのような形になっています。

unescapeHTML()では、タグを除去した上でescapeHTML()と逆の処理を行います。

ここでタグを除去しているのは、対象となる文字列が意味のあるHTMLタグが含まれない状態(escapeHTML()をした後の状態)である、という前提のためです。

空のdiv要素を作成し、そのinnerHTMLにstripTags()した文字列を入れます。その際、文字が存在する場合にはchildNodesが作成されるので、その中身を結合したものを返します。

IEなどではchildNodes[0]にテキストノードがひとつだけ入るので、それだけを返せばいいのですが、Firefoxで4096バイト以上のテキストをinnerHTMLに入れた場合に複数のchildNodesに分割される、という報告があったために、childNodes配列に対してinject()を適用することにより、全てのchildNodesの値を結合して返す、という実装となっています。

0289:   toQueryParams: function(separator) {
0290:     var match = this.strip().match(/([^?#]*)(#.*)?$/);
0291:     if (!match) return {};
0292: 
0293:     return match[1].split(separator || '&').inject({}, function(hash, pair) {
0294:       if ((pair = pair.split('='))[0]) {
0295:         var key = decodeURIComponent(pair.shift());
0296:         var value = pair.length > 1 ? pair.join('=') : pair[0];
0297:         if (value != undefined) value = decodeURIComponent(value);
0298: 
0299:         if (key in hash) {
0300:           if (hash[key].constructor != Array) hash[key] = [hash[key]];
0301:           hash[key].push(value);
0302:         }
0303:         else hash[key] = value;
0304:       }
0305:       return hash;
0306:     });
0307:   },
0308: 

toQueryParams()は、URIなどで使われるクエリ文字列のようなもの(key1=value1&key2=value2のような形式)を、プロパティと値を持つJavaScriptオブジェクトの形式に変換してくれる関数です。

引数のseparatorを省略すると、293行目の(separator || '&')にあるようにデフォルトで&が使われます。

まず290、291行目で、#文字以降とそれ以前に分けています。thisである文字列に#という文字があるとそれ以降は使われません。291行目が終わった段階で、match[1]に正規表現のグループ化の一つ目が入るので、ここが処理対象となります(match[0]はマッチした文字列全体⁠⁠。

そのmatch[1]に入っている文字列をseparator || '&'でsplit()して配列にします。それに対して{}という空オブジェクトを初期値としてinject()を実行します(inject()はEnumerable.injectです。後述します⁠⁠。

実行されるのが294~305行目のコード部分です。split()された結果がpairとして渡されるので、その文字列を=でさらにsplit()して再度pairに代入します。

そのpair[0]に値があるかどうかをif文で確認しています。ここが偽になるのは、'=value'のように'='文字の左側に文字列が無い場合です。その場合は次のペアに進みます。

295行目の段階でpairは配列で、pair[0]には=の左側、すなわち変数名にあたるもの、pair[1]には値にあたるものが入ります。

toQueryParams()は適切にURIエンコードされていることが前提なので、変数名にあたるものをデコードして変数keyに入れておきます。

値に'='という文字が入ってしまっていると、294行目でsplit()した際に分割された状態で配列になってしまうので、もしpair.shift()後のpair.lengthが1より大きければjoin()する、という形でvalueに単なる文字列として正規化された状態の値を保存します。

さらに、valueがundefined値でなければこちらもURIデコードしておきます。といいつつ、ここでvalueがundefined になる条件が思いつきません。

299行目で、返すhashに既にプロパティが存在しているかどうかを確認します。まだ存在しなければ単純にそのプロパティkeyに値valueを代入するだけですが、すでに存在する場合にはプロパティ値が配列になるように細工をします。300~301行目の処理により、同じ変数名が複数渡ってきた場合には、返されるオブジェクトのプロパティ値には配列が入るようになっています。

最終的に、ハッシュ形式のオブジェクトが返されます。

0309:   toArray: function() {
0310:     return this.split('');
0311:   },
0312: 
0313:   succ: function() {
0314:     return this.slice(0, this.length - 1) +
0315:       String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
0316:   },
0317: 
0318:   times: function(count) {
0319:     var result = '';
0320:     for (var i = 0; i < count; i++) result += this;
0321:     return result;
0322:   },
0323: 

toArray()は、単純にsplit('')を呼び出して、文字列を文字ごとの配列に変換します。

succ()は、Number.succ()と同様に「次の値」を返す関数ですが、Stringの場合は最後の文字の文字コードを一つ増やす、という挙動となります。特に繰り上がりのようなことは考慮していないようです。

たとえば"abc".succ()は"abd"という文字列を返します。

times()は、countという整数を引数に取り、指定された回数だけ文字列を繰り返し、それを結合したものを返します。Enumerable.inject()を使った方がきれいに書けそうですが、ここではfor文を使ったわかりやすい実装になっています。

0324:   camelize: function() {
0325:     var parts = this.split('-'), len = parts.length;
0326:     if (len == 1) return parts[0];
0327: 
0328:     var camelized = this.charAt(0) == '-'
0329:       ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
0330:       : parts[0];
0331: 
0332:     for (var i = 1; i < len; i++)
0333:       camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
0334: 
0335:     return camelized;
0336:   },
0337: 

camelize()は、単語を'-'でつないだ文字列をキャメルケース(camelCaseのような大文字小文字の使い方)に変換します。

まず'-'でsplit()して、326行目で'-'が含まれていなければそのままの文字列を返します。

328~330行目では、先頭が'-'であった場合の例外処理を行っています。通常のキャメルケースは小文字で始まりますが、ここでは'-abc-def'のように '-'で始まっていた文字列の場合には、先頭を大文字にしています。

332~333行目において、配列の残りの部分を先頭だけ大文字にして返り値となる camelized 変数に追加しています。各単語の先頭の文字以外はそのまま結合しているだけなので、"abc-dEf"などとすでに大文字になっているものは特にいじらず、"abcDEf"というようにそのまま返します。

あとはcamelized変数に入っている結果をreturnするだけです。

0338:   capitalize: function() {
0339:     return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
0340:   },
0341: 
0342:   underscore: function() {
0343:     return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
0344:   },
0345: 
0346:   dasherize: function() {
0347:     return this.gsub(/_/,'-');
0348:   },
0349: 

338行目からはcapitalize()メソッドです。インスタンス文字列の一文字目をtoUpperCase()を使って大文字にし、substring(1)を使って二文字目以降を取り出してtoLowerCase()で小文字にして、それらを結合したものを返します。

342行目からはunderscore()メソッドです。公式APIによると、キャメルケースの文字列を、単語ごとに'_'(下線)で区切られた文字列に変換する、とあります。

実装としては、インスタンス文字列に対して連続してgsub()関数を適用して、最後にtoLowerCase()で小文字にする、という処理になっています。

まず最初にgsub(/::/, '/')で'::'を'/'に変換します。これはPerlなどで名前空間を分割するのに用いられる'::'を、ファイル階層にマップするための置換、のように見えますが、underscore()でこのような処理が必要になる理由は不明です。

次にgsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}')で、一個以上の大文字が連続した後に一文字の大文字、さらに一文字の小文字が続いたものを、後ろ二文字とその前で分けて下線で区切ります。

ここまでで "camelCAse" という文字列が "camelC_Ase" となります。ここの処理は、大文字一文字の単語を分割するために必要となります。

最後にgsub(/([a-z\d])([A-Z])/,'#{1}_#{2}')で大文字から始まっている単語とそれ以前を下線で分割し、その結果をtoLowerCase()して返しています。ここまでで"camelCAse"という文字列は"camel_c_ase"となり、目的が達成されます。

346行目からのdasherize()メソッドは、単純に文字列中の下線"_"をダッシュ"-"にすべて置換するだけです。

0350:   inspect: function(useDoubleQuotes) {
0351:     var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
0352:       var character = String.specialChar[match[0]];
0353:       return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
0354:     });
0355:     if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
0356:     return "'" + escapedString.replace(/'/g, '\\\'') + "'";
0357:   },
0358: 

350行目からはinspect()メソッドです。これは他の型にも定義されているように、インスタンスの内容をわかりやすく出力するためのものです。

String型では、0x00から0x1Fまでの文字を、'\u001f'のように変換して返します。この際、202行目で定義されているString.specialCharオブジェクトに該当する特殊文字だった場合には、より一般的な表示文字列に変換します。

メソッドのuseDoubleQuotes引数に応じて、二重引用符か一重引用符で返し、その際中身の文字列中の引用符を適切にエスケープするようにしています。

0359:   toJSON: function() {
0360:     return this.inspect(true);
0361:   },
0362: 
0363:   unfilterJSON: function(filter) {
0364:     return this.sub(filter || Prototype.JSONFilter, '#{1}');
0365:   },
0366: 
0367:   isJSON: function() {
0368:     var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
0369:     return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
0370:   },
0371: 
0372:   evalJSON: function(sanitize) {
0373:     var json = this.unfilterJSON();
0374:     try {
0375:       if (!sanitize || json.isJSON()) return eval('(' + json + ')');
0376:     } catch (e) { }
0377:     throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
0378:   },
0379: 

359行目からの toJSON()メソッドは、他の型と同様に JSON としての文字列を返しますが、ここでの実装は単に inspect(true) を呼ぶだけです。この場合は二重引用符を使った文字列が返されます。

363行目からは unfilterJSON() メソッドです。引数として渡されたフィルタ関数か、デフォルトして使われる Prototype.JSONFilter 関数を使ってインスタンス文字列を置換します。

Prototype.JSONFilterを使う場合、'/*-secure-\n{"name": "Violet"}\n*/'という文字列が、'{"name": "Violet"}'という文字列になります。

このようなコメント形式で括られた文字列は、サーバ側から返される際に「簡単にJSON文字列に変換することができるが、<script>タグなどで直接読み込まれたとしても使うことができないようにする」という用途で使われることがあります。

367行目からはisJSON()メソッドです。正規表現を使って簡易チェックを行っているだけなので、厳密にJSONとして正しいかどうかをチェックしているわけではありません。

公式APIには"something".isJSON()がfalseを返す、となっており(文字列が引用符で括られていないため⁠⁠、数値表現に含まれる"a"などは許されているために"aaa".isJSON()はtrueを返してしまったりします。

この関数は元々Safariのバグに対処するためのものですが(Rails Trac ticket #7834⁠、'.'を'@'で置換している部分は理由がわかりません。

368行目で二重引用符で括られている文字列は問題ない、ということで消去し、のこった文字列に対して、JSON構文で使われる文字列しか含まれていないかどうか、という正規表現チェックが行われます。

372行目からはevalJSON()メソッドです。まず、unfilterJSON()をデフォルトのフィルタで呼び出して、保護用のコメント文字列を必要に応じて取り除きます。

375行目では、sanitize 引数が指定されていればisJSON()メソッドでチェックを行い、そうでなければそのままでeval()を呼び出します。

eval()時には、"("と")"で対象となる文字列を括っています。これは、例えばハッシュ形式のオブジェクトとして評価しようと"{'key':1}"という文字列をそのままeval()しようとすると、通常のコンテキストでは文を括るための中括弧、⁠gotoなどで使う)ラベル'key'、数値の1という形に解釈されます。この場合引用符付きの'key'という文字列はラベルとして不正なのでエラーとなってしまいます。

それを避けるために、変数として評価されるコンテキストを強制するために"(", ")"で括る、という手法がよく使われます。

注:

以前eval()が置かれる状況によって、中身が式のコンテキストで評価されるか文のコンテキストで評価されるかが変化してしまう、という状況に陥ったような記憶があるのですが、今回検証した限りでは文のコンテキストで評価されているようです。
ECMA-262 第三版の仕様を見ても、eval()内の文字列がどのコンテキストで評価されるべきであるか、という記述は見つけることができませんでした。

条件が偽となるか、eval()が例外を発生させると377行目に行きますが、そうでなければeval()の返り値をreturnで返します。

もし例外が発生したり、isJSON()が偽を返したりする場合には、SyntaxError型の例外をnewしてそれをthrowしています。

0380:   include: function(pattern) {
0381:     return this.indexOf(pattern) > -1;
0382:   },
0383: 
0384:   startsWith: function(pattern) {
0385:     return this.indexOf(pattern) === 0;
0386:   },
0387: 
0388:   endsWith: function(pattern) {
0389:     var d = this.length - pattern.length;
0390:     return d >= 0 && this.lastIndexOf(pattern) === d;
0391:   },
0392: 

380行目からのinclude()メソッドは、引数として渡された文字列がインスタンス文字列中に含まれているかどうかをbooleanで返します。実装としてはindexOf()に引数を渡して-1より大きいかどうかをチェックしているだけです(indexOf()は見つかると0以上の整数を返します⁠⁠。

384行目からのstartsWith()メソッドも定型句のショートカット関数で、インスタンス文字列が引数の文字列から始まっているかどうかをbooleanで返します。

わざわざ==ではなく===を使っているかは不明ですが、厳密な比較をしても特に害があるわけでもないので問題はないでしょう。

388行目からのendsWith()メソッドも同様ですが、引数として渡された文字列の方がインスタンス文字列よりも長い場合には、比べるだけ無駄なので先に偽を返します。そうでなければ、lastIndexOf()を使ってインスタンス文字列が指定された文字列で終わっているかどうかを確認します。正規表現で書いたほうがすっきりしますが、効率のためか、lastIndexOf() の返り値が(後ろから探して見つかった時のインデックス⁠⁠、⁠インスタンス文字列の長さ - 引数文字列の長さ)の値と等しいかどうか、で判断しています。

0393:   empty: function() {
0394:     return this == '';
0395:   },
0396: 
0397:   blank: function() {
0398:     return /^\s*$/.test(this);
0399:   }
0400: });
0401: 

393行目からのemtpy()メソッドは、インスタンス文字列が空文字列かどうかをbooleanで返すだけです。

397行目からのblank()メソッドは、インスタンス文字列にホワイトスペース文字のみが含まれているかどうかをbooleanで返します。

ちなみにJavaScriptにおけるホワイトスペースは、"15.10.2.12 CharacterClassEscape"において"the set of characters containing the characters that are on the right-hand side of the White Space (7.2) or Line Terminator (7.3) productions" として定義されています。

ここでWhiteSpaceには\u0009 (Tab)、\u000B (Vertical Tab)、\u000C (Form Feed)、\u0020 (Space)、\u00A0 (No-break space)、Unicode "space separator"が含まれます。最後の文字群は、日本語の範囲ではいわゆる全角スペースが含まれています。

また、Line Terminatorには\u000A (Line Feed)、\u000D (Carriage Return)、\u2028 (Line separator)、\u2029 (Paragraph separator)が含まれます。

  • ECMA-262 第三版 - 15.10.2.12 CharacterClassEscape
  • ECMA-262 第三版 - 7.2 White Space
  • ECMA-262 第三版 - 7.3 Line Terminators
0402: if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
0403:   escapeHTML: function() {
0404:     return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
0405:   },
0406:   unescapeHTML: function() {
0407:     return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
0408:   }
0409: });
0410: 

String型の、escapeHTML()、unescapeHTML()メソッドをブラウザを判別した上で上書きしています。

元々の関数は、ブラウザの挙動を使った興味深い実装でしたが、Prototype.Browser.WebKit、Prototype.Browser.IEに合致するブラウザの場合には、replace()を使ったシンプルな関数に置き換えられています。

これは、これらのブラウザでは元々の関数のやり方では、スペースが保存されず、変換結果が期待通りにならないため、のようです。

0411: String.prototype.gsub.prepareReplacement = function(replacement) {
0412:   if (typeof replacement == 'function') return replacement;
0413:   var template = new Template(replacement);
0414:   return function(match) { return template.evaluate(match) };
0415: }
0416: 
0417: String.prototype.parseQuery = String.prototype.toQueryParams;
0418: 

411行目からは、すでに定義されているString.prototype.gsub関数オブジェクトのプロパティとして、prepareReplacementという関数オブジェクトを定義しています。どう使われているかはString.prototype.gsub()メソッドの項を参照してください。

417行目では、toQueryParams()メソッドの別名としてparseQuery()を定義しています。

0419: Object.extend(String.prototype.escapeHTML, {
0420:   div:  document.createElement('div'),
0421:   text: document.createTextNode('')
0422: });
0423: 
0424: with (String.prototype.escapeHTML) div.appendChild(text);
0425: 

419行目からは、String.prototype.escapeHTML()メソッドのプロパティとして、div、textを定義しています。これらの使われ方はescapeHTML()の項を参照してください。

424行目もescapeHTML()で使われるdiv、textの事前準備です。ここでwith句を使っているのは、String.prototype.escapeHTML.div.appendChild (String.prototype.escapeHTML.text)を短く記述するためです。

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()が真を返す、という形になります。

0483:   collect: function(iterator) {
0484:     var results = [];
0485:     this.each(function(value, index) {
0486:       results.push((iterator || Prototype.K)(value, index));
0487:     });
0488:     return results;
0489:   },
0490: 

483行目からはcollect()メソッドです。each()を使って渡された関数を呼び出し、(iterator || Prototype.K)(value, index)の呼び出しの返り値をresults配列に集めてそれを返します。

0491:   detect: function(iterator) {
0492:     var result;
0493:     this.each(function(value, index) {
0494:       if (iterator(value, index)) {
0495:         result = value;
0496:         throw $break;
0497:       }
0498:     });
0499:     return result;
0500:   },
0501: 

491行目からはdetect()メソッドです。each()で各要素をチェックし、iterator()関数オブジェクトを実行します。この返り値が真の場合にはresultに要素値を代入した上でthrow $breakでループを抜けます。

もし、interatorが真を返すものが無かった場合、resultの初期値が返ります。492行目でvar resultとなっているので初期値はundefinedとなっています。

0502:   findAll: function(iterator) {
0503:     var results = [];
0504:     this.each(function(value, index) {
0505:       if (iterator(value, index))
0506:         results.push(value);
0507:     });
0508:     return results;
0509:   },
0510: 

502行目からはfindAll()メソッドです。grepのより柔軟なバージョンです。

最初に空の配列resultsを用意して、each()を使ってiteratorで渡された関数が真を返すとresultsにその値をpush()して、最後にそのresultsを返します。

0511:   grep: function(pattern, iterator) {
0512:     var results = [];
0513:     this.each(function(value, index) {
0514:       var stringValue = value.toString();
0515:       if (stringValue.match(pattern))
0516:         results.push((iterator || Prototype.K)(value, index));
0517:     })
0518:     return results;
0519:   },
0520: 

511行目からはgrep()メソッドです。findAll()が一致判定用の関数オブジェクトを渡して判断するのに対して、grep()では比較対象を文字列に変換した上で比較します。

引数patternはString.match()に渡されるので、正規表現オブジェクトを使うことができます。引数iteratorに何も指定しないとPrototype.Kが使われるので、Enumerableインスタンスの各要素値がpush()されますが、iteratorに関数オブジェクトを渡すとその返り値がpush()されるようになっています。

0521:   include: function(object) {
0522:     var found = false;
0523:     this.each(function(value) {
0524:       if (value == object) {
0525:         found = true;
0526:         throw $break;
0527:       }
0528:     });
0529:     return found;
0530:   },
0531: 

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

引数として渡されたobjectが、Enumerableの中に含まれているかどうかをチェックしてbooleanで返します。

522行目でfoundの初期値としてfalseを入れておき、each()のループで一致するものが見付かるとfoundにtrueを入れてthrow $breakでループを抜けます。

この時、include()では==演算子を使っていますので、必要に応じて暗黙の型変換が入る場合があります。

0532:   inGroupsOf: function(number, fillWith) {
0533:     fillWith = fillWith === undefined ? null : fillWith;
0534:     return this.eachSlice(number, function(slice) {
0535:       while(slice.length < number) slice.push(fillWith);
0536:       return slice;
0537:     });
0538:   },
0539: 

532行目からはinGroupsOf()メソッドです。eachSlice()メソッドに似ていて、こちらは等サイズに分割した後の余った部分に値を埋めるようになっているので、配列の最後の要素もふくめて全て等しいサイズになります。

533行目でまずfillWithを正規化します。inGroupsOf()の呼び出し時にfillWith引数が指定されなかった場合、fillWith === undefinedが真になります。その場合はnullを代入しておき、そうでなければ元のfillWithをそのまま使います。

あとはeachSlice()を呼び出し、その際にiteratorとして関数を渡し、その中で指定されたサイズに満たない場合にはfillWithで指定された値を補充しています。

0540:   inject: function(memo, iterator) {
0541:     this.each(function(value, index) {
0542:       memo = iterator(memo, value, index);
0543:     });
0544:     return memo;
0545:   },
0546: 

540行目からはinject()メソッドです。Enumerable内の要素に対して順々に何らかの処理を行っていく関数です。

引数memoに渡されるのが初期値で、iterator関数を542行目のような引数を伴って呼び出します。この関数が返す値がmemoとして引き続き使われるようになっています。

このinject()は、ライブラリ内で頻繁に用いられており、ある状態変数を保持した上で各要素に対して何らかの処理を行う、という際に活躍します。

0547:   invoke: function(method) {
0548:     var args = $A(arguments).slice(1);
0549:     return this.map(function(value) {
0550:       return value[method].apply(value, args);
0551:     });
0552:   },
0553: 

547行目からはinvoke()メソッドです。

Enumerableインスタンス内の各要素値に対して、指定されたメソッドを呼び出します。

まず548行目で、invoke()メソッドに渡された二番目以降の引数を取り出して配列としてargsに格納します。これにより可変長の引数を渡すことができます。

後は、map()を使ってインスタンス内の各要素値を引数に550行目の関数を呼び出します。呼び出したい関数名が文字列としてmethodという変数に入っているので、通常のvalue.methodのような形の呼び出しはできませんが、メソッドもプロパティ値として格納されていることには変わりないのでvalue[method]とすることで該当する関数オブジェクトを参照することができます。これに対してapply()を呼び出すことで、該当する関数の呼び出しとなります。この際第一引数のvalueがその関数内でのthisとなります。

0554:   max: function(iterator) {
0555:     var result;
0556:     this.each(function(value, index) {
0557:       value = (iterator || Prototype.K)(value, index);
0558:       if (result == undefined || value >= result)
0559:         result = value;
0560:     });
0561:     return result;
0562:   },
0563: 
0564:   min: function(iterator) {
0565:     var result;
0566:     this.each(function(value, index) {
0567:       value = (iterator || Prototype.K)(value, index);
0568:       if (result == undefined || value < result)
0569:         result = value;
0570:     });
0571:     return result;
0572:   },
0573: 

554行目からはmax()メソッドです。

まず555行目でvar resultを定義します。この段階で中身はundefinedとなります。

その状態でeach()でループを開始し、iteratorが指定されていればその関数が返す値を、そうでなければEnumerable内の値をそのまま比較対照とします。

result にまだ何も入っていない(undefined⁠⁠、もしくはresultよりvalueが大きければresultを更新します(ここでのundefinedとの比較は===を使ったほうが安全な気がします⁠⁠。

最後に残ったresultをmax()の返り値として返します。このため、要素のないEnumerableに対してmax()を呼び出すと、undefinedが返されます。

564行目からはmin()メソッドです。数値比較の部分が>=から<になっている以外はmax()とまったく同一です。

0574:   partition: function(iterator) {
0575:     var trues = [], falses = [];
0576:     this.each(function(value, index) {
0577:       ((iterator || Prototype.K)(value, index) ?
0578:         trues : falses).push(value);
0579:     });
0580:     return [trues, falses];
0581:   },
0582: 

574行目からはpartition()メソッドです。

引数で渡されるinterator関数が真と判断するもの、偽と判断するものに分割し、2要素の配列として返します。

まず575行目で空のtrues、falses配列を用意します。その後、each()でループし、各要素に対して iterator関数を呼び出します。返り値が真か偽かによってtruesかfalsesの配列が返るような三項演算子を使い、その評価値に対してpush()して値を詰め込んでいます。

最後に[ trues, falses ]という配列を返して終了です。

0583:   pluck: function(property) {
0584:     var results = [];
0585:     this.each(function(value, index) {
0586:       results.push(value[property]);
0587:     });
0588:     return results;
0589:   },
0590: 

583行目からはpluck()メソッドです。

各要素の中から指定されたプロパティの値をまとめて返す関数です。

584行目でresultsとして空の配列を用意し、each()を使ってループします。ループ内では単に指定されたプロパティ値を取り出し、resultsにpush()しています。最後にresultsを返します。

0591:   reject: function(iterator) {
0592:     var results = [];
0593:     this.each(function(value, index) {
0594:       if (!iterator(value, index))
0595:         results.push(value);
0596:     });
0597:     return results;
0598:   },
0599: 

591行目からはreject()メソッドです。

findAll()メソッドとほぼ同一で、iterator関数の条件の真偽のみが異なります。結果として、iteratorが偽を返す要素のみをresultsに集めて返しています。

0600:   sortBy: function(iterator) {
0601:     return this.map(function(value, index) {
0602:       return {value: value, criteria: iterator(value, index)};
0603:     }).sort(function(left, right) {
0604:       var a = left.criteria, b = right.criteria;
0605:       return a < b ? -1 : a > b ? 1 : 0;
0606:     }).pluck('value');
0607:   },
0608: 

600行目からはsortBy()メソッドです。指定されたiterator関数が返す値に基づいてソートした配列を返します。

601行目にいきなりreturnが来ていますが、ここではthisに対してmap()、sort()、pluck()を連続して呼び出して、その結果を返しています。

601行目のmap()では、まず要素を{ value:要素値, criteria:iterator関数の返り値 }というオブジェクトに変換し、そのまま配列として返します。

603行目では、標準のArray.sort()関数に渡して、605行目の三項演算子でsort()関数が要求する-1, 0, 1を返しています。

606行目では、そのソート関数が返すソート済みのオブジェクトを、pluck()を使って、予め値が格納してあるvalueプロパティの値を取り出してその配列として返します。

このmap -> sort -> map(今回はpluck())というステップは、Perlの世界ではシュワルツ変換、と呼ばれているものです。

0609:   toArray: function() {
0610:     return this.map();
0611:   },
0612: 

609行目のtoArray()メソッドは、単にmap()メソッドを呼んで返しているだけです。この関数は多くの場合Enumerableを継承したクラスにより上書き定義されます。

0613:   zip: function() {
0614:     var iterator = Prototype.K, args = $A(arguments);
0615:     if (typeof args.last() == 'function')
0616:       iterator = args.pop();
0617: 
0618:     var collections = [this].concat(args).map($A);
0619:     return this.map(function(value, index) {
0620:       return iterator(collections.pluck(index));
0621:     });
0622:   },
0623: 

613行目からはzip()メソッドです。

この関数は、任意の数を引数として受け付けつつ、最後のiterator関数オブジェクトがオプショナルな引数となっています。

そのためまず614行目でiteratorにデフォルト値としてPrototype.Kを代入しておき、argsにargumentsをArray化したものを代入しておきます。argsの最後の要素のtypeofが'function'ならばiteratorが指定されているとみなして、iterator変数にpop()します。

618行目で、zipされる配列の配列を作ります。たとえば[1,2,3].zip([4,5,6],[7,8,9])と呼び出された場合は、collectionsには[[1,2,3],[4,5,6],[7,8,9]]という値が入ります。

ここで、元のEnumerableインスタンスに対してmap()を適用し、各々に対して620行目の処理を行います。ここで、indexは0から呼ばれるごとに増えていくので、最初は0, 次は1, その次は2 となります。collections.pluck(0)の呼び出しは、collectionsの要素のプロパティ"0"の値を返すので、上記の例では[1,4,7]という配列が返り、それがiterator()に渡されます。

これがthis.length分繰り返され、最終的にzipされた配列が返されます(上記例では[[1,4,7],[2,5,8],[3,6,9]]という値⁠⁠。

0624:   size: function() {
0625:     return this.toArray().length;
0626:   },
0627: 

624行目からはsize()メソッドです。Enumerableインスタンスの長さを返します。

toArray()を呼び出してlengthプロパティを取る、という形を取っているのは、Enumerableの実体がArrayとは限らないので、いったんArrayにしているからでしょう。Enumerable.toArray()はeach()を呼び出すのでO(n)の性能になってしまいます。

実際にはEnumerableがmixinされるクラスの側で効率の良い関数として上書きされることが多いようです。

0628:   inspect: function() {
0629:     return '#<Enumerable:' + this.toArray().inspect() + '>';
0630:   }
0631: }
0632: 

628行目からはinspect()メソッドです。ここではEnumerableのinspect()が呼び出されている、ということがわかるようにテキスト表記されたものが返されます。

0633: Object.extend(Enumerable, {
0634:   map:     Enumerable.collect,
0635:   find:    Enumerable.detect,
0636:   select:  Enumerable.findAll,
0637:   member:  Enumerable.include,
0638:   entries: Enumerable.toArray
0639: });

633行目からはEnumerableが提供する関数のエイリアスが5つ定義されています。これらはどっちがメインでどっちがおまけ、という区別はないようで、Prototypeライブラリの中でもどちらが使われているかは一定していません。

おすすめ記事

記事・ニュース一覧