ECMA-262 第三版について
第二回目です。
本連載中には、
仕様書としてはわかりにくい部類に入るかな、
また、
では、
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オブジェクト直下に直接追加しているので、
199行目のString.
これにより、
result += String.interpret(replacement(match));
specialCharには特殊文字列と、
String.
String オブジェクトへの拡張
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にマッチする文字列を見つけたら、
215行目でreplacementを関数オブジェクトにしている所が多少わかりにくいかもしれません。
ここで、
- 単純な置換後の文字列
- RegExp.
exec()の返す配列を引数として受け取り、 受け取り置換後の文字列を返すFunctionオブジェクト - Prototypeライブラリが提供するTemplateクラスのオブジェクト
のどれが渡されたとしても、
……という挙動を意図しているのだと思われますが、
例えば以下のようになります。
var s = "ABC";
var output = s.gsub('ABC', 'Template では #{...} と書きます。');
alert(output); # 'Template では と書きます。' が出力される
- 公式 API String.
gsub - 公式 API Template
- ECMA-262 第三版 - 15.
10. 6.2 RegExp. prototype. exec(string)
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.
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()を呼び出しているだけですが、
truncate()では、
Stringオブジェクトでは内部がUnicodeなので、
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()では、
stripTags()では
< | <自身 |
\/? | あってもなくてもいい / 文字 (これで開くタグ、 |
[^>]+ | タグ終端となる > 文字以外が1文字以上連続したもの |
> | >自身 |
gi | g は置換をグローバルに、 |
という内容になっていて、
stripScripts()では、
<script[^>]*> | <script>タグ(属性があっても可) |
([\\S\\s]*?) | [\\S\\s]で任意の文字 |
<\/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.
まず、
これらは同じ正規表現ルールを用い、
gが付いているmatchAllをString.
そして、 (this.
次に、
ここで、
その各々に対して、
すると、
match()が失敗した場合は['','']という配列を返すようにして、
これをextractScript()の呼出し側に返すことで、
271行目からのevalScript()では、
これにより、
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()では、
ここでは単純にreplace()などを使うのではなく、
276行目のvar selfはarguments.
別途もう少し後の419行目あたりにおいて、
これにより、
ただし、
これは、
このメソッドescapeHTML()は、
unescapeHTML()では、
ここでタグを除去しているのは、
空のdiv要素を作成し、
IEなどではchildNodes[0]にテキストノードがひとつだけ入るので、
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()は、
引数のseparatorを省略すると、
まず290、
そのmatch[1]に入っている文字列をseparator || '&'でsplit()して配列にします。それに対して{}という空オブジェクトを初期値としてinject()を実行します
実行されるのが294~305行目のコード部分です。split()された結果がpairとして渡されるので、
そのpair[0]に値があるかどうかをif文で確認しています。ここが偽になるのは、
295行目の段階でpairは配列で、
toQueryParams()は適切にURIエンコードされていることが前提なので、
値に'='という文字が入ってしまっていると、
さらに、
299行目で、
最終的に、
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()は、
succ()は、
たとえば"abc".succ()は"abd"という文字列を返します。
times()は、
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()は、
まず'-'でsplit()して、
328~330行目では、
332~333行目において、
あとは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()を使って大文字にし、
342行目からはunderscore()メソッドです。公式APIによると、
実装としては、
まず最初にgsub(/::/, '/')で'::'を'/'に変換します。これはPerlなどで名前空間を分割するのに用いられる'::'を、
次にgsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}')で、
ここまでで "camelCAse" という文字列が "camelC_
最後にgsub(/([a-z\d])([A-Z])/,'#{1}_#{2}')で大文字から始まっている単語とそれ以前を下線で分割し、
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型では、
メソッドの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()メソッドは、
363行目からは unfilterJSON() メソッドです。引数として渡されたフィルタ関数か、
Prototype.
このようなコメント形式で括られた文字列は、
367行目からはisJSON()メソッドです。正規表現を使って簡易チェックを行っているだけなので、
公式APIには"something".isJSON()がfalseを返す、
この関数は元々Safariのバグに対処するためのものですが
368行目で二重引用符で括られている文字列は問題ない、
372行目からはevalJSON()メソッドです。まず、
375行目では、
eval()時には、
それを避けるために、
- 注:
以前eval()が置かれる状況によって、
ECMA-262 第三版の仕様を見ても、
条件が偽となるか、
もし例外が発生したり、
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()メソッドは、
384行目からのstartsWith()メソッドも定型句のショートカット関数で、
わざわざ==ではなく===を使っているかは不明ですが、
388行目からのendsWith()メソッドも同様ですが、
0393: empty: function() {
0394: return this == '';
0395: },
0396:
0397: blank: function() {
0398: return /^\s*$/.test(this);
0399: }
0400: });
0401:
393行目からのemtpy()メソッドは、
397行目からのblank()メソッドは、
ちなみにJavaScriptにおけるホワイトスペースは、
ここでWhiteSpaceには\u0009 (Tab)、
また、
- 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,'&').replace(/</g,'<').replace(/>/g,'>');
0405: },
0406: unescapeHTML: function() {
0407: return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
0408: }
0409: });
0410:
String型の、
元々の関数は、
これは、
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行目からは、
417行目では、
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行目からは、
424行目もescapeHTML()で使われるdiv、
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.
427行目では、
429行目からがクラスのコンストラクタです。引数としてtemplate, patternを取ります。evaluate()メソッド実行時のために、
434行目からが実際にテンプレート置換を行うevaluate()メソッドです。コンストラクタで渡されたtemplate文字列に対して、
ここでTemplate.
0443: var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead');
0444:
443行目では、
$breakは、
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は、
446行目からはeach()メソッドです。派生クラスで定義される_each()メソッドを使って、
この際、
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は配列の分割処理のインデックスですが、
461行目でnumber個ごとにarrayを分割してslicesにpush()し、
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をセットします。
その後、
468行目において、
その後、
全ての要素中で評価関数がひとつも偽とならなかった場合には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()と逆で、
外枠のループはall()とほぼ同様で、
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()を使って渡された関数を呼び出し、
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()で各要素をチェックし、
もし、
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を用意して、
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()が一致判定用の関数オブジェクトを渡して判断するのに対して、
引数patternはString.
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が、
522行目でfoundの初期値としてfalseを入れておき、
この時、
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引数が指定されなかった場合、
あとはeachSlice()を呼び出し、
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に渡されるのが初期値で、
この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行目で、
後は、
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()でループを開始し、
result にまだ何も入っていない
最後に残ったresultをmax()の返り値として返します。このため、
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関数が真と判断するもの、
まず575行目で空のtrues、
最後に[ 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として空の配列を用意し、
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()メソッドとほぼ同一で、
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が来ていますが、
601行目のmap()では、
603行目では、
606行目では、
このmap -> sort -> map
0609: toArray: function() {
0610: return this.map();
0611: },
0612:
609行目のtoArray()メソッドは、
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()メソッドです。
この関数は、
そのためまず614行目でiteratorにデフォルト値としてPrototype.
618行目で、
ここで、
これがthis.
0624: size: function() {
0625: return this.toArray().length;
0626: },
0627:
624行目からはsize()メソッドです。Enumerableインスタンスの長さを返します。
toArray()を呼び出してlengthプロパティを取る、
実際には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つ定義されています。これらはどっちがメインでどっちがおまけ、