現代的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