現代的JavaScriptおれおれアドベントカレンダー2017 – 13日目
概要
昨日のやつでJavaScriptのテンプレート記法をご紹介したわけなんだけども、そのテンプレート記法を使った処理を「タグ関数」を作って独自に拡張することができます。
// 連結するだけの(非現実的な)タグ関数 function tagger(template, ...substitutions) { console.log(template[0]); // => "hoge " console.log(template[1]); // => " fuga " console.log(template[2]); // => " piyo" console.log(substitutions[0]); // => 10 console.log(substitutions[1]); // => 25 return template[0] + substitutions[0] + template[1] + substitutions[1] + template[2]; } const n = 10; console.log( `hoge ${n} fuga ${20 + 5} piyo`); // => "hoge 10 fuga 25 piyo" console.log(tagger`hoge ${n} fuga ${20 + 5} piyo`); // => "hoge 10 fuga 25 piyo"
...substitutions
は可変長引数的なやつです。別稿参照。
タグ関数
テンプレート記法を分解した結果を受け取り、処理して返す関数です。
デフォルトで String.raw()
というものがあります。
console.log(String.raw`foo\nbar\n${"boo"}`); // => "foo\\nbar\\nboo"
この関数を自作することができます。
例えば普通に繋げるだけの「何もしない」タグ関数を作る場合、こんな感じになります。
function plain(strings, ...values) { // strings[0] === "hoge " // values[0] === 10 // strings[1] === " fuga " // values[1] === 25 // strings[2] === " piyo" // 文字列を連結してゆく変数 // (初期値として先頭のもの "hoge " を与えておく。) let result = strings[0]; // 先頭は既に追加済みなので飛ばし、 // 残りを順次追加 for (var i = 1; i < strings.length; i++) { result += values[i - 1] + strings[i]; } return result; } const n = 10; console.log( `hoge ${n} fuga ${20 + 5} piyo`); // => "hoge 10 fuga 25 piyo" console.log(plain`hoge ${n} fuga ${20 + 5} piyo`); // => "hoge 10 fuga 25 piyo"
values
には ${
… }
の部分が、 strings
にはそうではない普通の部分がそれぞれ配列で格納されます。
strings.length
は values.length + 1
に必ずなります。なるはず。
例えば `A${99}Z`
の場合、 strings
は ["A", "Z"]
、 values
は [99]
となります。
普通の文字列部分がない場合
文字がない場所は空文字列になります。
`${99}`
の場合、 ${
… }
の前後に空文字列があるとみなされます。つまり strings
は ["", ""]
、 values
は [99]
となります。
同様に `${-1}${99}`
の場合、 strings
は ["", "", ""]
、 values
は [-1, 99]
となります。
一行で普通に繋げるやつ(おまけ)
こんな感じでどっすかね。
// 普通に結合 function plain(strings, ...values) { return strings.slice(1).reduce((result, s, i) => result + values[i] + s, strings[0]); } console.log(plain`hoge ${1} fuga ${2} piyo`)
Array#reduce()らくちんちょうべんり。(読みやすいかは別だけど。)
与えられた数値の小数点以下を揃える
${
… }
には数値を与えられる前提で、各値の小数点以下二桁まで表示する、というやつをやってみたいと思います。
// decimal-regularized text function drt(strings, ...values) { let result = ''; for (let i = 0; i < strings.length; i++) { result += strings[i]; const value = values[i]; if (typeof value === 'number') { const sValue = Math.floor(value * 100).toString(); // 3.1415 -> 314.15 -> 314 -> "314" const srValue = `${sValue.slice(0, -2)}.${sValue.slice(-2)}`; // "314" -> "3" + "." + "14" result += srValue; } } return result; } console.log(drt`πは${Math.PI}です。`); // => "πは3.14です。" console.log(drt`私の身長は${167.3}cmです。`); // => "私の身長は167.30cmです。" console.log(drt`私の体重は${66}kgです。`); // => "私の体重は66.00kgです。"
タグ関数を返す関数
前項の「与えられた数値の小数点以下を揃える」やつ、何桁で揃えたいかってあるじゃないですか。そこを引数で受け取れるようにすると便利かもしれません。
というわけで、タグ関数を(クロージャを使って生成して)返す関数を用意してみました。
// decimal-regularized text function drt(digits) { // この中身は、digitsを使ってる以外はさっきのと一緒 return function(strings, ...values) { let result = ''; for (let i = 0; i < strings.length; i++) { result += strings[i]; const value = values[i]; if (typeof value === 'number') { const sValue = Math.floor(value * 10 ** digits).toString(); // 3.1415 -> 314.15 -> 314 -> "314" const srValue = `${sValue.slice(0, -digits)}.${sValue.slice(-digits)}`; // "314" -> "3" + "." + "14" result += srValue; } } return result; } } const drt1 = drt(1); console.log(drt1`πは${Math.PI}です。`); // => "πは3.1です。" const drt5 = drt(5); console.log(drt5`πは${Math.PI}です。`); // => "πは3.14159です。"
文字列以外を返す
例えばHTML文字列を受け取ったら、それからDOMツリーを構築して返す、なんてのもありだと思います。
function html(strings, ...values) { // 普通に結合 const html = strings[0] + strings.reduce((result, s, i) => values[i-1] + s, ''); // DOMツリー生成 // (サボってjQuery使います。) const tree = jQuery(html)[0]; return tree; } const title = 'Hello World!'; document.body.appendChild(html` <div class="main"> <h1>${title}</h1> </div>`)
テンプレート関数
関数を返すタグ関数なんてのもいけます。
さっきのを拡張して、任意のデータを受け取ってDOMツリーを形成するやつにしました。
// データ const items = [ { name: 'Sugoi pen', price: '3000' }, { name: 'Sugokunai pen', price: '130' }, ]; // 「DOMを生成する関数」を返すタグ関数 function html(strings, ...keys) { // DOMを生成する関数 return function(data) { // 結合 let html = strings[0]; for (var i = 1; i < strings.length; i++) { const key = keys[i - 1]; const value = data[key]; html += value + strings[i]; } // DOMツリー生成 // (サボってjQuery使います。) const tree = jQuery(html)[0]; return tree; }; } // テンプレート const itemTemplate = html` <li class="sugoi-item"> ${'name'} <span class="price">(${'price'}円)</span> </li> `; // 実行 const elList = document.querySelector('ul#yabai-list'); items.forEach(data => { elList.appendChild(itemTemplate(data)); });
Mustacheで <h1>{{title}}</h1>
と書いていたところを <h1>${'title'}</h1>
と書くようにした感じです。
デモ:
普通の文字列を解析してあれこれしてオブジェクト生成して返す
昔作ったのを思い出しました。
Vue.jsでコンポーネントが上位コンポーネントから値を受け取る (props
) とき、その値の妥当性を検証する仕組みがあります。こんな感じで諸々指定していく感じです。
const MyComponent = { props: { simpleString: { type: String }, requiredNumber: { type: Number, required: true }, defaultBoolean: { type: Boolean, default: true }, } }
これをもうちょっと簡単に書けないかなーと思って作りました。こういうふうに書けます。
const pt = require('vue-props-template') const MyComponent = { props: pt` string simpleString required number requiredNumber boolean defaultBoolean = ${true} ` }
文字列を元にオブジェクトを返しているわけです。
その後全然触ってないや。年末にまたやろうかな。
うっ頭が
その他
raw
String.raw()
を普通に関数呼び出しすると、こんな感じだとエラーになってしまう。
String.raw(['foo', 'bar'], 1); // Exception: TypeError: can't convert undefined to object
どうも実は第一引数の配列に raw
というプロパティが追加されていて、そちらを見ているみたい。ので、それを与えてやれば大丈夫。
String.raw({ raw: ['foo', 'bar'] }, 1); // => "foo1bar"
この raw
はエスケープをものともしないやつらが格納されてます。下記別稿「生文字列」の項を参照。
自作のタグ関数から strings.raw
を見るのも可能。使い道はぱっと思いつかないけど、 String.raw()
的なことをしたい場面で便利なはずです。
function rawSomething(strings, ...values) { const rawStrings = strings.raw; // ... }
テンプレート記法の解析とオブジェクト生成
“12.2.9.3 Runtime Semantics: GetTemplateObject ( templateLiteral )” がその仕様だと思うんだけど、正直理解し切れてません。
Realmってなんじゃ……。
参考
- ECMAScript® 2017 Language Specification
- 12.3.7 Tagged Templates
- 21.1.2.4 String.raw ( template, …substitutions )
- 12.3.6.1 Runtime Semantics: ArgumentListEvaluation
- 12.2.9.1 Static Semantics: TemplateStrings
- 12.2.9.3 Runtime Semantics: GetTemplateObject ( templateLiteral ) … テンプレート記法の解析、だと思う。
strings.raw
の追加とか
- テンプレート文字列 – JavaScript | MDN
- String.raw() – JavaScript | MDN