初めに
今回はテンプレートリテラルの使い方をまとめていきたいと思います。
Template literals(Template strings)
Basic usage
すでに使い慣れたと思ったけど、実際調べたら別の使い方や応用もあるので色々とメモしたいです。
HTML
のタグとタグの間空白があっても画面に表示されないのであまり気にしませんでしたが、意識しないうちにそれがバグの原因にあったかもしれないので、trim()
で空白を排除するのがいいと思います。
let str = `
<ul>
<li>first</li>
<li>second</li>
</ul>
`.trim()
console.log(str)
/*
<ul>
<li>first</li>
<li>second</li>
</ul>
*/
テンプレートリテラルにはどんな値が入れられても最終的に文字列になる。
let obj = {
x: 1,
y: 2
};
let re = {
x: `${obj.x}`,
y: `${obj.y}`
}
console.log(re)
// { x: '1', y: '2' } // value is string
テンプレートリテラルの中でもテンプレートリテラルを用いられる。(入れ子関数みたいなものです。)
let userName = [
{ first: 'Taro', last: 'Yamada' },
{ first: 'Jiro', last: 'Yamada' }
];
let temp = (address) => `
<table>
${address.map((x) => `
<tr><td>${x.first}</td></tr>
<tr><td>${x.last}</td></tr>
`).join('').trim()}
</table>
`.trim();
console.log(temp(userName))
/*
<table>
<tr><td>Taro</td></tr>
<tr><td>Yamada</td></tr>
<tr><td>Jiro</td></tr>
<tr><td>Yamada</td></tr>
</table>
*/
下のように、暗黙の''
よりテンプレートリテラルの方が自由に微調整できるし安全です。例えばAPI
から貰ったURL
の行頭や末尾に/
が含んだら組み立て時、毎回splice()
などのメソッドで正しい文字列を抽出しなければならない。また、結合演算子+
がもたらした型変換の問題もなくなります。(JSONファイルから取り出しや宣言で型を固定しているなら問題にならないが。)
const base = 'http://example.com/resouces/';
const path = '/example.js';
function getResource(baseURL, pathName) {
return `${baseURL.replace(/\/$/, '')}${pathName}`
}
console.log(getResource(base, path));
// http://example.com/resouces/example.js
Tagged template(Tagged function)
タグつきテンプレート関数です。まだ経験の浅い自分に初めてみた使い方ですが、今回も頭を抱えながら自分なりにまとめてみました。
タグ付きテンプレートの内部はどうやって実装しているかは初めてみたときさっぱりでした。いくつの参考文章を読み書きながら大まかにまとめると、
- タグ付きテンプレートは普通の文字列の部分とパラメータ(引数)を分けている。
- 実装のイメージは
reduce()
のように最終的に一つの文字列に結合する。(for
ループでも実現できる。)
まずは普通のテンプレートとタグ付きテンプレートの違いから比べてみたいと思います。一般の使い方では関数の引数としてテンプレート入れて出力させたら、
function fullName(str) {
console.log(str)
}
fullName(`First name is ${'Taro'}, last name is ${'Yamada'}`);
// First name is Taro, last name is Yamada
タグ付きテンプレートなら
function fullName(first, ...last) {
console.log(arguments);
console.log(first);
console.log(last);
}
fullName`First name is ${'Taro'}, last name is ${'Yamada'}.`
/*
[Arguments] {
'0': [ 'First name is ', ', last name is ', '.' ],
'1': 'Taro',
'2': 'Yamada'
}
*/
// ['First name is ', ', last name is ', '.']
// ['Taro', 'Yamada']
// note: /First name is /${'Taro'}/, last name is /${'Yamada'}/./
上のように、タグ付きテンプレートは${}
が区切りになって、普通の文字列と${}
の引数が分けられている。
ではどのように結合して出力するのでしょうか。
// reduce((previousValue, currentValue, currentIndex))
function stringRaw(strings, ...values) {
console.log(arguments)
return strings.reduce((first, str, i) => {
console.log([first, values[i - 1], str]);
return first + values[i - 1] + str;
});
}
// console.log(stringRaw`First name is ${'Taro'}, last name is ${'Yamada'}.`)
// [Arguments] {
// '0': [ 'First name is ', ', last name is ', '.' ],
// '1': 'Taro',
// '2': 'Yamada'
// }
// ['First name is ', 'Taro', ', last name is ']
// ['First name is Taro, last name is ', 'Yamada', '.']
// First name is Taro, last name is Yamada.
タグ付きテンプレートは文字列と引数を分けて配列にするから、[Arguments]
のように、strings
は普通の文字列の配列、...values
は残りの引数を展開する。(引数の個数が固定している場合はvalue1
, value2
などでパラメータ設置してもいいです。)
そしてreduce()
の特性を用いて、(first, str, i)
という設置で、
一回目のreduce()
はfirst
(指定しなければstrings[0]がデフォルト)、引数の配列[0]と、strings[1]が結合し、
/First name is /${'Taro'}/, last name is /
、となる。これは二回目のreduce()
のfirst
になります。
それで二回目はfirst
、引数の配列[1]、strings[2]が結合、
/First name is ${'Taro'}, last name is /${'Yamada'}/./
最終的に、First name is Taro, last name is Yamada.
となりました。
for
ループでもタグ付きテンプレートの実装ができます。
// First name is Taro, last name is Yamada.
let raw = function (strings, ...values) {
let output = '';
let i;
for (i = 0; i < values.length; i++) {
output += strings[i] + values[i];
console.log(output)
}
output += strings[i]
return output;
}
console.log(raw`First name is ${'Taro'}, last name is ${'Yamada'}.`)
// First name is Taro
// First name is Taro, last name is Yamada
// First name is Taro, last name is Yamada.
reduce()
で結合の原理が分かればfor
ループでも簡単に実現できます。ただfor
はreduce()
と少し違って、strings
の最後の要素をループ外でoutput
と結合しなければならない。これはタグ付きテンプレートが文字列への切り取り方と関わっている。タグ付きテンプレートの最後が${}
じゃなく単なる空白''
でも切り取るのです。不思議に思ったが、最初は手抜きして.
を書いてなかったんでこれに気づきました。つまりfor
ループはstrings
最後の要素をカバーできないのです。(<= values.length
にしてカバーしたら今度はvalues[]
がundefined
になる。)
そしてタグ付きテンプレートではなく、普通の関数の入れ方に戻すと、
console.log(raw(['First name is ', ', last name is ', '.'], 'Taro', 'Yamada'))
// First name is Taro
// First name is Taro, last name is Yamada
// First name is Taro, last name is Yamada.
もし複雑な処理をする必要ないならString.raw``
でも簡単にできる。
console.log(String.raw`First name is ${'Taro'}, last name is ${'Yamada'}.`)
// First name is Taro, last name is Yamada.
String.raw``
は\n
、\u
などのエスケープを無効化して生の文字列に形成してくれるメソッドです。もともとエスケープできず問題を起こすが今は修正されたそうです。
構造と使い方が分かったら、タグ付きテンプレートはメリットの多いメソッドです。例えばユーザ(input
から取り出したテキストとか)からの悪意のあるコードもろ過しすることができます。
function saferHTML(strings, ...values) {
let result = ''
let i
for (i = 0; i < values.length; i++) {
let value = String.raw`${values[i]}`
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
result += strings[i] + value
}
return (result + strings[i]);
}
let sender = '<script>alert("abc")</script>'; // malicious code
let message = saferHTML`<p>${sender} has sent you a message.</p>`;
console.log(message)
// <p><script>alert("abc")</script> has sent you a message.</p>
encodeURIComponent
エスケープされないもの:A-Z a-z 0-9 - _ . ! ~ * ' ( )
encodeURIComponent()
はURI
(Uniform Resource Identifier)構成要素として特定の文字をUTF-8
エンコーディングのようにエスケープシーケンスに転換するメソッドです。例えばURL
には特定の符号文字? = / &
、または漢字などUTF-8
にエンコードしないと、訪問先のパスとして識別できないということです。
さきのようにString.raw``
でURL
の処理をしたら、
const input = 'A&B';
const escapedURL = String.raw`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL)
// https://example.com/search?q=A&B&sort=desc
console.log(encodeURIComponent(input))
// A%26B
String.raw``
はエスケープ無しで生の文字列として処理してくれるだけです。
タグ付きテンプレート関数を使えばユーザからもらったinput
テキストを関数内部で処理する。
function escapeURL(strings, ...values) {
return strings.reduce((result, str, i) => {
let value = String.raw`${values[i - 1]}`
return result + encodeURIComponent(value) + str;
});
}
const input = 'A&B';
const escapedURL = escapeURL`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL)
// https://example.com/search?q=A%26B&sort=desc
for
ループで書き換えてもいいです。
function escapeURL(strings, ...values) {
let output = '';
let i;
for (i = 0; i < values.length; i++) {
let value = String.raw`${values[i]}`
output += strings[i] + encodeURIComponent(value);
}
output += strings[i]
return output;
}
const input = 'A&B';
const escapedURL = escapeURL`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL)
// https://example.com/search?q=A%26B&sort=desc
HTMLはUTF-8
でエンコードしているため、encodeURIComponent()
の応用はURL
のエスケープのほかにもたくさんあります。
自分はこれまでの勉強ではサロゲート、サロゲートペアなどという文字列の表示にまつわる問題と処理も少し理解しつつ実践してきましたが、今回テンプレートの勉強ではUTF-8
とUTF-16
の認識はまだまだ足りないと感じて今後の課題にしておきたいと思います。