模板字面量进阶:Tagged Templates
标签模板允许你自定义模板字面量的解析逻辑。
javascript
function i18n(strings, ...values) {
// strings: 字面量片段数组;values: 插值值数组
return strings.reduce((acc, s, i) => acc + s + (values[i] ?? ""), "");
}
const name = "Elara";
console.log(i18n`你好,${name}!`);
1
2
3
4
5
6
7
2
3
4
5
6
7
注意转义与安全(例如 XSS 处理)。
1. 标签函数的签名与 raw
标签函数会收到两个参数序列:
strings
:由字面量片段构成的数组(不可变);...values
:每个${}
的计算结果,按顺序传入。
同时,strings.raw
提供“原始”字面量(不会处理如 \n
的转义)。
javascript
function debug(strings, ...values) {
console.log("strings:", strings);
console.log("raw :", strings.raw);
console.log("values :", values);
return strings.reduce((acc, s, i) => acc + s + (values[i] ?? ""), "");
}
debug`A\nB ${1 + 1} C`;
// strings: [ 'A\nB ', ' C' ]
// raw : [ 'A\\nB ', ' C' ] // 注意双反斜杠,表示原始字面量中的转义序列
// values : [ 2 ]
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2. 安全 HTML 插值(防 XSS)
标签模板可作为“安全插值”函数,自动转义用户输入,避免直接拼接到 innerHTML
时的风险。
javascript
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function html(strings, ...values) {
return strings.reduce(
(acc, s, i) => acc + s + (i < values.length ? escapeHtml(values[i]) : ""),
""
);
}
const user = { name: "<img onerror=alert(1)>" };
const safe = html`<div class="user">${user.name}</div>`;
// => <div class="user"><img onerror=alert(1)></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
提示:若需要允许部分安全 HTML,可结合白名单过滤或使用可信模板库。
3. i18n 与格式化
结合 Intl
可实现更强的本地化格式化:
javascript
function fmt(locale, options = {}) {
const nf = new Intl.NumberFormat(locale, options.number);
const dt = new Intl.DateTimeFormat(locale, options.date);
return function template(strings, ...values) {
const cooked = values.map((v) =>
typeof v === "number"
? nf.format(v)
: v instanceof Date
? dt.format(v)
: v
);
return strings.reduce((acc, s, i) => acc + s + (cooked[i] ?? ""), "");
};
}
const zh = fmt("zh-CN", { number: { style: "currency", currency: "CNY" } });
console.log(zh`总价:${1234.5}`);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4. 缩进裁剪与多行字符串整形
常见需求:保持源码缩进,但输出时去掉公共缩进或首尾空行。
javascript
function trimIndent(strings, ...values) {
const text = strings.reduce((acc, s, i) => acc + s + (values[i] ?? ""), "");
const lines = text
.replace(/^\n/, "")
.replace(/\n\s*$/, "")
.split("\n");
const indents = lines
.filter((l) => l.trim())
.map((l) => l.match(/^\s*/)[0].length);
const base = indents.length ? Math.min(...indents) : 0;
return lines.map((l) => l.slice(base)).join("\n");
}
const block = trimIndent`
任务:
- 收集 10 颗宝石
- 打败史莱姆王
`;
console.log(block);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
5. 动态正则构建(小心转义)
javascript
function re(strings, ...values) {
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = strings.reduce(
(acc, s, i) => acc + s + (i < values.length ? esc(values[i]) : ""),
""
);
return new RegExp(pattern, "u");
}
const word = "a+b";
const rx = re`^${word}$`;
console.log(rx.test("a+b")); // true
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
6. 带配置的标签(柯里化)
javascript
const colorize =
(color = "black") =>
(strings, ...values) =>
strings.reduce((acc, s, i) => acc + s + (values[i] ?? ""), "") +
` [${color}]`;
console.log(colorize("green")`OK ${200}`); // OK 200 [green]
1
2
3
4
5
6
7
2
3
4
5
6
7
7. 性能与可维护性
- 标签函数每次调用都会遍历
strings
和values
,热点路径谨慎使用; - 将通用逻辑封装为库函数(如
html
、trimIndent
)并单元测试; - 对安全相关的标签(HTML/SQL)要有明确的白名单/转义策略。
8. 容易踩坑
- 忘记处理转义/注入风险,直接把
${}
值拼到敏感上下文中; - 使用
raw
时误以为是“未转义字符串”,实际上是“原始字面量内容”(包含\\n
这样的序列); - 在运行时构建极其复杂的标签逻辑,导致难调试、难优化。
9. 最佳实践
- 将标签模板用于“模式化”的字符串构建(HTML、日志、i18n、格式化),统一策略;
- 对需要可读性的多行内容使用
trimIndent
一类工具; - 安全相关标签严格单元测试,覆盖边界与攻击样例。
10. 练习
- 实现一个
sql
标签:自动把${}
参数替换为占位符并收集参数数组(仅教学,实际请用专业库)。 - 实现一个
md
标签:将${}
中的特殊字符转义,避免破坏 Markdown 结构。 - 基于
Intl
实现一个格式化标签,支持${number}
与${Date}
的本地化显示。