現代的JavaScriptおれおれアドベントカレンダー2017 – 13日目
概要
昨日のやつでJavaScriptのテンプレート記法をご紹介したわけなんだけども、そのテンプレート記法を使った処理を「タグ関数」を作って独自に拡張することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 連結するだけの(非現実的な)タグ関数 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()
というものがあります。
1 | console.log(String.raw`foo\nbar\n${ "boo" }`); // => "foo\\nbar\\nboo" |
この関数を自作することができます。
例えば普通に繋げるだけの「何もしない」タグ関数を作る場合、こんな感じになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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]
となります。
一行で普通に繋げるやつ(おまけ)
こんな感じでどっすかね。
1 2 3 4 5 6 | // 普通に結合 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()らくちんちょうべんり。(読みやすいかは別だけど。)
与えられた数値の小数点以下を揃える
${
… }
には数値を与えられる前提で、各値の小数点以下二桁まで表示する、というやつをやってみたいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 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です。" |
タグ関数を返す関数
前項の「与えられた数値の小数点以下を揃える」やつ、何桁で揃えたいかってあるじゃないですか。そこを引数で受け取れるようにすると便利かもしれません。
というわけで、タグ関数を(クロージャを使って生成して)返す関数を用意してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 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ツリーを構築して返す、なんてのもありだと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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ツリーを形成するやつにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // データ 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
) とき、その値の妥当性を検証する仕組みがあります。こんな感じで諸々指定していく感じです。
1 2 3 4 5 6 7 | const MyComponent = { props: { simpleString: { type: String }, requiredNumber: { type: Number, required: true }, defaultBoolean: { type: Boolean, default : true }, } } |
これをもうちょっと簡単に書けないかなーと思って作りました。こういうふうに書けます。
1 2 3 4 5 6 7 8 9 | const pt = require( 'vue-props-template' ) const MyComponent = { props: pt` string simpleString required number requiredNumber boolean defaultBoolean = ${ true } ` } |
文字列を元にオブジェクトを返しているわけです。
その後全然触ってないや。年末にまたやろうかな。
うっ頭が
その他
raw
String.raw()
を普通に関数呼び出しすると、こんな感じだとエラーになってしまう。
1 | String.raw([ 'foo' , 'bar' ], 1); // Exception: TypeError: can't convert undefined to object |
どうも実は第一引数の配列に raw
というプロパティが追加されていて、そちらを見ているみたい。ので、それを与えてやれば大丈夫。
1 | String.raw({ raw: [ 'foo' , 'bar' ] }, 1); // => "foo1bar" |
この raw
はエスケープをものともしないやつらが格納されてます。下記別稿「生文字列」の項を参照。
自作のタグ関数から strings.raw
を見るのも可能。使い道はぱっと思いつかないけど、 String.raw()
的なことをしたい場面で便利なはずです。
1 2 3 4 | 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