タグ付きテンプレートとは何か
タグ付きテンプレートは、テンプレート文字列の先頭に“関数名”を置き、文字列と式の分解結果をその関数で“加工してから”最終文字列を得る仕組みです。ここが重要です:通常のテンプレートが「埋め込んで終わり」なのに対し、タグ付きでは“埋め込む前の段階”でエスケープ・整形・国際化・インデント調整などを一括で制御できます。
function tag(strings, ...values) {
// strings: 文字列断片の配列(生の文字列)
// values: ${...} の式の評価結果の配列
return strings.reduce((out, str, i) => out + str + (values[i] ?? ""), "");
}
const name = "Alice";
const msg = tag`Hello, ${name}!`;
console.log(msg); // "Hello, Alice!"
JavaScript仕組みの要点(strings と values、raw の違い)
strings と values の対応関係
テンプレートは「文字列断片」と「式の値」が交互に並びます。ここが重要です:タグ関数は strings[i] の後ろに values[i] を挿入しながら連結します。最後の文字列断片には対応する値がないため、values[i] ?? "" のように欠番を安全に扱います。
function joinTag(strings, ...vals) {
return strings.reduce((s, str, i) => s + str + (vals[i] ?? ""), "");
}
const out = joinTag`A:${1} B:${2}`;
console.log(out); // "A:1 B:2"
JavaScript生文字列(raw)を使うか、加工後(cooked)を使うか
strings.raw は“エスケープ前の生文字列”を返します。ここが重要です:\n を本当にバックスラッシュ + n として受け取りたい、正規表現やデデント処理で必要になる場合に raw が役立ちます。
function showRaw(strings) {
console.log(strings.raw); // 生の文字列断片
}
showRaw`\n行頭`; // ["\\n行頭"] のように表示
JavaScriptHTML サニタイズ(XSS対策のための重要パターン)
危険な文字を安全に置き換える
テンプレートにユーザー入力を埋め込むと XSS の危険があります。ここが重要です:タグ関数で < > & " ' をエスケープしてから連結する仕組みにすれば、常に安全な HTML を生成できます。
const escape = s => String(s).replace(/[&<>"']/g, c => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
}[c]));
function html(strings, ...vals) {
const safe = vals.map(escape);
return strings.reduce((out, str, i) => out + str + (safe[i] ?? ""), "");
}
const user = `<Alice>`;
const comment = `こんにちは "世界"`;
const view = html`<p>User: ${user}</p><p>${comment}</p>`;
console.log(view); // 安全なエスケープ済み HTML
JavaScriptインデント調整(デデントで見た目と出力を両立)
コードのインデントは保ち、出力は左端に揃える
テンプレートは改行・空白をそのまま保持します。ここが重要です:見やすくコードをインデントしても、出力は“最小インデント幅”分だけ左へ寄せるデデント関数をタグ付きで実装できます。
function dedent(strings, ...vals) {
// まず通常の連結
let text = strings.reduce((o, s, i) => o + s + (vals[i] ?? ""), "");
// 先頭/末尾の空行を整理
text = text.replace(/^\n+/, "").replace(/\n+$/, "\n");
// 最小インデント幅を検出
const lines = text.split("\n");
const widths = lines
.filter(l => l.trim().length)
.map(l => l.match(/^(\s*)/)[1].length);
const min = widths.length ? Math.min(...widths) : 0;
// 左へ寄せる
return lines.map(l => l.slice(min)).join("\n");
}
const html = dedent`
<div>
<h1>Title</h1>
<p>Body</p>
</div>
`;
console.log(html);
/*
<div>
<h1>Title</h1>
<p>Body</p>
</div>
*/
JavaScript整形・国際化・安全な属性埋め込みの応用
数値・日付の整形は補助関数に分離
テンプレート内で直接書くと読みづらい整形は、タグ関数と補助関数で分離します。ここが重要です:タグは“安全と構造”、補助関数は“ロジック”に責務を分けます。
const yen = n => `${n.toLocaleString("ja-JP")} 円`;
function htmlF(strings, ...vals) {
return strings.reduce((o, s, i) => o + s + (vals[i] ?? ""), "");
}
const card = ({ id, name, price }) => htmlF`
<div class="card" data-id="${id}">
<h2>${name.trim()}</h2>
<p>価格: ${yen(price)}</p>
</div>
`;
console.log(card({ id: 1, name: " Apple ", price: 123456 }));
JavaScript安全な属性値を作る
属性はクォート閉じやスクリプト注入の入り口になります。ここが重要です:属性専用のエスケープで「ダブルクォート」と「< > &」を重点的に防御します。
const escAttr = s => String(s).replace(/[&<>"]/g, c => ({
"&": "&", "<": "<", ">": ">", '"': """
}[c]));
function attr(strings, ...vals) {
const safe = vals.map(escAttr);
return strings.reduce((o, s, i) => o + s + (safe[i] ?? ""), "");
}
const btn = `<button class="buy" data-id=${attr`${7}`} title=${attr`購入 "安全"`}>購入</button>`;
console.log(btn);
JavaScriptよくある落とし穴と回避策(重要ポイントの深掘り)
タグ関数の戻り値が“文字列以外”だと、その後の利用箇所(innerHTML など)で想定外になります。常に文字列を返すこと。values の要素がオブジェクトの場合、暗黙の文字列化で [object Object] になるため、必要に応じて JSON 化や独自フォーマットを行うこと。raw を扱うときは、エスケープが未適用のことを理解して使用範囲を限定すること。重い処理や非同期呼び出しをタグ関数内で直接行うと、評価タイミングが分かりにくくなるため、原則としてタグ関数は“軽く・純粋”に保つこと。
// オブジェクトはそのまま埋め込まない
function safeJSON(strings, ...vals) {
const conv = v => typeof v === "object" ? JSON.stringify(v) : v;
const vv = vals.map(conv);
return strings.reduce((o, s, i) => o + s + (vv[i] ?? ""), "");
}
JavaScript例題で理解を固める
// 1) HTML サニタイズタグ
function html(strings, ...vals) {
const escape = s => String(s).replace(/[&<>"']/g, c => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
}[c]));
const safe = vals.map(escape);
return strings.reduce((o, s, i) => o + s + (safe[i] ?? ""), "");
}
const user = `<Alice>`;
const page = html`
<section>
<h1>ようこそ、${user} さん</h1>
<p>安全な表示ができます。</p>
</section>
`;
console.log(page);
// 2) デデント+配列の合成
function dedent(strings, ...vals) {
let text = strings.reduce((o, s, i) => o + s + (vals[i] ?? ""), "");
text = text.replace(/^\n+/, "").replace(/\n+$/, "\n");
const lines = text.split("\n");
const widths = lines.filter(l => l.trim()).map(l => l.match(/^(\s*)/)[1].length);
const min = widths.length ? Math.min(...widths) : 0;
return lines.map(l => l.slice(min)).join("\n");
}
const items = [{ id: 1, price: 100 }, { id: 2, price: 200 }];
const table = dedent`
<table>
${items.map(r => dedent`
<tr>
<td>${r.id}</td>
<td>${r.price.toLocaleString("ja-JP")} 円</td>
</tr>
`).join("")}
</table>
`;
console.log(table);
// 3) 属性専用タグで安全にボタン生成
const escAttr = s => String(s).replace(/[&<>"]/g, c => ({
"&": "&", "<": "<", ">": ">", '"': """
}[c]));
const attr = (strings, ...vals) =>
strings.reduce((o, s, i) => o + s + (vals[i] != null ? escAttr(vals[i]) : ""), "");
const button = `
<button class="buy" data-id="${attr`${7}`}" title="${attr`購入 "特価"`}">購入</button>
`;
console.log(button);
JavaScriptまとめ
タグ付きテンプレートの核心は「テンプレートの“文字列断片”と“式の値”を、タグ関数の中で安全・整然に加工してから連結できる」ことです。strings と values の対応を正しく扱い、必要なら raw を使って生文字列を処理する。HTML 生成ではサニタイズタグを用意して XSS を防ぎ、インデントはデデントで整え、属性値は専用エスケープで安全にする。タグ関数は軽く純粋に保ち、整形ロジックは補助関数へ分離する。この指針を徹底すれば、初心者でもテンプレート文字列を“安全・読みやすい”設計で使いこなせます。
