【ES2015】テンプレートリテラルのタグについての基礎
今更ながらES2015のテンプレートリテラルに、タグ という機能があることを知ったので、それについて調べてみようと思います。
テンプレートリテラルとは
まずは簡単にテンプレートリテラルについて振り返ってみたいと思います。
Template literal は組み込み式を扱うことができる文字列リテラルです。複数行文字列や文字列内挿機能を使用できます。ES2015 / ES6 仕様の以前のエディションでは、"template strings" と呼ばれていました。
(MDNより)
ざっくりいうと、リテラル内で変数や関数を展開することができます。
ES5までは文字列と変数との連結には +
(プラス)を使っていて、やや複雑な記述になりがちでしたが、ES2015以降ではこのテンプレートリテラルのおかげでシンプルに書けるようになりました。
下記は、APIコールする時のリクエストするURLを作成するサンプルです。
const END_POINT = 'https://hoge.com/api/'; const param1 = 'hoge'; const param2 = 'fuga'; // es5までの+演算子で連結した書き方 const url1 = END_POINT + '?param1=' + param1 + '¶m2=' + param2; // テンプレートリテラルでの書き方 const url2 = `END_POINT?param1=${param1}¶m2=${param2}`;
このように、 `
(バッククォート) で囲うとその範囲はテンプレートリテラルとなります。
その中では、${param1}
のようにすることで変数を展開できます。
また関数も実行することができます。
function greet() { return 'Hello'; } console.log(`${greet()}, world!`); // Hello, world!
その外にも便利な機能として、リテラル内で改行することも可能です。
// es5までの書き方 var htmlEs5 = '<dl>' + '<dt>ES5までの書き方<dt>' + '<dd>+による連結をしなくてはいけないので面倒です。<dd>' + '</dl>'; console.log(htmlEs5 ); /* 実行結果 <dl><dt>ES5までの書き方<dt><dd>+による連結をしなくてはいけないので面倒です。インデントも考慮するとなおさらです。<dd></dl> */ // テンプレートリテラルでの書き方 const htmlEs2015 = `<dl> <dt>テンプレートリテラルでの書き方</dt> <dd>そのまま改行が行えるので簡単です</dd> </dl>`; console.log(htmlEs2015 ); /* 実行結果 <dl> <dt>テンプレートリテラルでの書き方</dt> <dd>そのまま改行が行えるので簡単です</dd> </dl> */
もちろん今までのエスケープもそのまま使えます。
const htmlEs5 = '<p>\nHello\n</p>'; const htmlEs2015 = `<p>\nHello </p>`; console.log(htmlEs2015 ); /* 実行結果 <p> Hello </p> */
ざっくりと振り返りは以上です。
タグとは
タグとはただの関数 のことで、テンプレートリテラルの直前に関数名をつけることで、その関数の処理を通した結果の文字列を生成できます。
この構文のことを、そのままですが、タグ付きテンプレートリテラル(Tagged Templates)と言います。
簡単なサンプルで確認してみます。
function tag (strings, ...values) => { console.log(strings); // ["私の名前は", "です。年齢は", "歳です。"] console.log(values); // ["トム", 20] return 'hoge'; }; const name = 'トム'; const age = 20; console.log(tag `私の名前は${name}です。年齢は${age}歳です。`); // hoge
tag
関数 の第一引数strings
にはテンプレートリテラルの${}
以外の箇所が配列として入っています。tag
関数 の第二引数...values
には${}
の値が配列として入っています。 (スプレッド演算子を使わずに、tag (strings, value1, value2)
として記述することも可能です。しかしこの書き方だと、${}
が増えるたびに関数も修正する必要が出てしまうため、...values
として配列で受け取ってしまうのがスタンダードのようです)tag
関数の返り値が最終的な出力結果になっている
ちなみに、${}
の前後に文字列がない場合は空文字が渡されます。
function tag (strings, ...values) { console.log(strings, values); }; const name = 'トム'; const age = 20; const result1 = tag `${name}${age}`; // ["", "", ""], ["トム", 20] const result2 = tag `${name}です。${age}`; // ["", "です。", ""], ["トム", 20]
タグを使ってテンプレートリテラル内の変数を大文字にする例
function toUpper (strings, ...values) { let res = ''; for (let i = 0; i < strings.length; i += 1) { res += strings[i]; if (i < values.length) { res += values[i].toUpperCase(); } } return res; } const name = 'Tom'; const country = 'japan'; const food = 'sushi'; console.log(toUpper `My name is ${name}. I live in ${country}. I like ${food}.`); // My name is TOM. I live in JAPAN. I like SUSHI.
生の文字列にアクセスする
タグの第一引数に渡される値は、基本的にエスケープ処理された値になります。
そのため\n
などは改行コードに変換されています。
function test(strings, ...values) { console.log(strings); // ["Hello,↵world."] } test `Hello,\nworld.`;
場合によっては変換されたくない場合があるかと思います。
そのような生の文字列にアクセスするには、タグのrawプロパティからアクセス します。
タグの第一引数は raw
プロパティというものを持っていて、下記のように生の文字列にアクセス可能です。
function test(strings, ...values) { console.log(strings.raw); // ["Hello,\nworld."] } test `Hello,\nworld.`;
もう少しわかりやすい例を示します。
function test(strings, ...values) { console.log(strings); // ["a↵", "b↵", ""] console.log(strings.raw); // ["a\n", "b\n", ""] } test `a\n${1}b\n${2}`;
test関数第一引数のstrings
内は図で示すと、下記のような状態です。
String.raw()メソッド
String.raw()
はテンプレートリテラルを文字列に組み立てて返す、唯一の組み込み(ビルトイン)タグ関数です。
構文
String.raw `templateString` String.raw({ raw: 'string' }, ...values);
テンプレートリテラルをそのまま渡した場合の1つめの構文の例です。
全てが連結されて、生の文字列が表示されているのが分かります。
console.log(String.raw `a\nb\n${1 + 1}`); // a\nb\n2
次に2つめの構文を使った例です。
文字をただ繋げて返すだけの処理です。
function concat(strings, ...values) { return String.raw(strings, ...values); } console.log(concat `a${1}b${1 + 1}c${1 + 2}\n`); // a1b2c3\n
仕組みを追ってみます。
引数strings
の値は ["a", "b", "c", "↵"]
という配列ですが、 stringsにはrawプロパティがあるので、下記のように値が渡されることになります。
String.raw({ raw: ["a", "b", "c", "↵"] }, ...values);
...values
はスプレッド演算子なので実際に展開されると下記のように構文に沿った形となります。
String.raw({ raw: ["a", "b", "c", "\n"] }, 1, 2, 3);
String.raw()
は、第一引数の1つ目の値、第二引数の値、第一引数の2つ目の値、第三引数の値、、、というように交互に組み立てるため文字列として出力されます。
これまでの流れを図で示します。
なお、これまでの処理は下記のようにシンプルに記述することが可能です。
function concat() { return String.raw.apply(null, aruguments); }
使いどころ
String.raw()
の使いどころとしては、その仕組みから テンプレートリテラルの文字列と変数の両方に同じ処理をしたい場合 に重宝できるようです。
また、Array.reduce()
などを使って自分で文字列を組み立てるという手間を省くことが可能です。
下記はHTMLのエスケープ処理の関数です。
function escape() { return String.raw.apply(null, arguments) .replace(/&/g, '&') .replace(/>/g, '>') .replace(/</g, '<') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/`/g, '`'); } const comment = `<script>alert('XSS攻撃');</script>`; const html = `<div>${escape `${comment}`}</div>` console.log(html); // <div><script>alert('XSS攻撃');</script></div>
まとめ
やや慣れが必要そうですが、タグ付きテンプレートを使うことで、複雑な文字列操作をシンプルにまとめることができそうです。
今後積極的に使っていきたいところです。