prototype.jsを読み解く

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

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

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文字として数えます。

著者プロフィール

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

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

コメント

コメントの記入