JavaScriptでテンプレート文字列を作る
今つくっているプログラムで必要そうだったので書いた。
やりたいこと
"$0...$1の年収、$2..?"
みたいなテンプレートがあって、それに $0=うわっ, $1=私, $2=低すぎ を渡すと、「うわっ...私の年収、低すぎ..?」と変換したい。
案1: string.replace
template = "$0...$1の年収、$2..?"; $0 = "うわっ"; $1 = "私"; $2 = "低すぎ"; // incorrect result = template.replace("$0", $0); // correct result = template.replace(/\$0/g, $0); result = result.replace(/\$1/g, $1); result = result.replace(/\$2/g, $2);
簡単にできる。JavaScript の replace は何故か1つしか置換してくれないので、最初の行のやり方は良くない。正規表現にグローバルオプションをつけて置換してあげれば良い。ただ、replaceする度に対象箇所を探しているので非効率な感じがする。テンプレートっていうのは置換したい場所は把握しているもんだ。毎回探すとかアホか。あと、言ってなかったけどバックスラッシュに続く $ はエスケープしたい。しかし、JavaScript の 正規表現 は何故か後読みしてくれないので、後の行のやり方も良くない。
案2: toString + join
JavaScript のオブジェクトには toString っていうメソッドがあって、文字列に変換したいときはこれが呼ばれる。そして配列には join っていうメソッドがあって、配列の中身を「文字列に変換して」くっつける。
つまり、こうできる。
$0 = {value:"ええっ" , toString:function() {return this.value;}}; $1 = {value:"お前" , toString:function() {return this.value;}}; $2 = {value:"そんなにあるの", toString:function() {return this.value;}}; template = [$0, "...", $1, "の年収、", $2, "..?"]; result = template.join("");
こっちのほうが早い。簡単に速度を計測したものを最後に載せておく。
コード
テンプレート文字列のクラスを作った。$0-$9まで使えて、エスケープもできる。
function TemplateString() { var str, defaultValue; if (arguments.length === 1) { str = arguments[0]; defaultValue = []; } else if (arguments.length >= 2) { str = arguments[0]; defaultValue = arguments[1]; } this.vars = (function(n, defaultValue) { var list = []; var i, imax; for (i = 0; i < n; i++) { list[i] = {value:defaultValue[i]||"", toString:function(){return this.value;}}; } return list; }(10, defaultValue)); this.set(str); } TemplateString.prototype.toString = function() { if (this.withT) { return this.value.join(""); } else { return this.value; } }; TemplateString.prototype.setvalue = function(index, value) { if (0 <= index && index < this.vars.length) { this.vars[index].value = value; } }; TemplateString.prototype.set = function(str) { if (/[^\\]\$[0-9]/.test(" " + str)) { this.withT = true; this.value = this._make(str); } else { this.withT = false; this.value = str; } }; TemplateString.prototype._make = function(str) { var list; var c, esc, dol, buffer; var i, imax; list = []; buffer = []; for (i = 0, imax = str.length; i < imax; i++) { c = str[i]; if (c === "$" && !esc) { dol = true; } else if (c === "\\") { esc = true; } else { if (dol && "0" <= c && c <= "9") { if (buffer.length > 0) { list.push(buffer.join("")); buffer = []; } list.push(this.vars[c|0]); } else { buffer.push(c); } esc = false; dol = false; } } if (buffer.length > 0) list.push(buffer.join("")); return list; };
速度比較
短い文字列 {うわっ}...{私}の年収、{低すぎ}..?
50000回 | 2500000回 | |
replace | 110msec | 4330msec |
toSting+join | 40msec | 2330msec |
TemplateString | 50msec | 2540msec |
長い文字列 走れメロスの冒頭677文字
500000回 | |
replace | 4550msec |
TemplateString | 1740msec |
var limit = 50000; (function(limit) { var template, lis, $0, $1, $2; var d, i, result; lis = ["うわっ", "私", "低すぎ"]; d = +new Date(); template = "$0...$1の年収、$2..?"; for (i = 0; i < limit; i++) { $0 = lis[(i+0)%3]; $1 = lis[(i+1)%3]; $2 = lis[(i+2)%3]; result = template.replace(/\$0/g, $0); result = result.replace(/\$1/g, $1); result = result.replace(/\$2/g, $2); } console.log("TIME", (+new Date() - d), result); }(limit)); (function(limit) { var template, lis, $0, $1, $2; var d, i, result; lis = ["うわっ", "私", "低すぎ"]; d = +new Date(); $0 = {value:"", toString:function() {return this.value;}}; $1 = {value:"", toString:function() {return this.value;}}; $2 = {value:"", toString:function() {return this.value;}}; template = [$0, "...", $1, "の年収、", $2, "..?"]; for (i = 0; i < limit; i++) { $0.value = lis[(i+0)%3]; $1.value = lis[(i+1)%3]; $2.value = lis[(i+2)%3]; result = template.join(""); } console.log("TIME", (+new Date() - d), result); }(limit)); (function(limit) { var template = "$0...$1の年収、$2..?"; var lis; var d, i, tstr ,result; lis = ["うわっ", "私", "低すぎ"]; d = +new Date(); tstr = new TemplateString(template); for (i = 0; i < limit; i++) { tstr.setvalue(0, lis[(i+0)%3]); tstr.setvalue(1, lis[(i+1)%3]); tstr.setvalue(2, lis[(i+2)%3]); result = tstr.toString(); } console.log("TIME", (+new Date() - d), result); }(limit)); console.log("----------------------------------------"); (function() { var limit = 500000; var template = "$0は激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。$0には政治がわからぬ。$0は、村の牧人である。笛を吹き、羊と遊んで暮して来た。けれども邪悪に対しては、人一倍に敏感であった。きょう未明$0は村を出発し、野を越え山越え、十里はなれた此のシラクスの市にやって来た。$0には父も、母も無い。女房も無い。十六の、内気な$2と二人暮しだ。この$2は、村の或る律気な一牧人を、近々、花婿として迎える事になっていた。結婚式も間近かなのである。$0は、それゆえ、花嫁の衣裳やら祝宴の御馳走やらを買いに、はるばる市にやって来たのだ。先ず、その品々を買い集め、それから都の大路をぶらぶら歩いた。$0には竹馬の友があった。$1である。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。歩いているうちに$0は、まちの様子を怪しく思った。ひっそりしている。もう既に日も落ちて、まちの暗いのは当りまえだが、けれども、なんだか、夜のせいばかりでは無く、市全体が、やけに寂しい。のんきな$0も、だんだん不安になって来た。路で逢った若い衆をつかまえて、何かあったのか、二年まえに此の市に来たときは、夜でも皆が歌をうたって、まちは賑やかであった筈だが、と質問した。若い衆は、首を振って答えなかった。しばらく歩いて老爺に逢い、こんどはもっと、語勢を強くして質問した。老爺は答えなかった。$0は両手で老爺のからだをゆすぶって質問を重ねた。老爺は、あたりをはばかる低声で、わずか答えた。"; var $0 = "メロス"; var $1 = "セリヌンティウス"; var $2 = "妹"; (function(limit, template, lis) { var d, i, result; d = +new Date(); for (i = 0; i < limit; i++) { result = template.replace(/\$0/g, lis[(i+0)%3]); result = result.replace(/\$1/g, lis[(i+1)%3]); result = result.replace(/\$2/g, lis[(i+2)%3]); } console.log("TIME", (+new Date() - d), result.substr(0, 20)); }(limit, template, [$0, $1, $2])); (function(limit, template, lis) { var d, i, tstr ,result; d = +new Date(); tstr = new TemplateString(template); for (i = 0; i < limit; i++) { tstr.setvalue(0, lis[(i+0)%3]); tstr.setvalue(1, lis[(i+1)%3]); tstr.setvalue(2, lis[(i+2)%3]); result = tstr.toString(); } console.log("TIME", (+new Date() - d), result.substr(0, 20)); }(limit, template, [$0, $1, $2])); }());