1. 引言:从 + 拼接说起
在 ES6 之前,我们拼接字符串基本靠 +:
javascript
const name = "小明";
const age = 18;
const msg = "我叫" + name + ",今年" + age + "岁。";
这段代码能跑,但问题也很明显:引号、加号、空格混在一起,稍长一点就难以阅读;换行要写 \n;变量多了更是灾难。
ES6 引入的**模板字符串(Template Literals)**用反引号 ````` 包裹,一举解决了多行与插值两大痛点。而它的进阶形态------标签模板(Tagged Templates),则把「字符串拼接」升级成了「可编程的字符串处理」,成为安全转义、国际化、语法高亮等场景的底层基石。
本文从基础语法讲到标签模板的实战应用,帮你把这块内容彻底吃透。
先看一张全景图,理清本文的知识脉络:
2. 多行字符串:告别 \n
模板字符串最直观的好处,就是天然支持换行,写什么就是什么:
javascript
const html = `
<div class="card">
<h2>标题</h2>
<p>内容</p>
</div>
`;
对比传统写法:
javascript
// 旧写法:需要手动拼接 \n,缩进也很别扭
var html = '<div class="card">\n' +
' <h2>标题</h2>\n' +
' <p>内容</p>\n' +
'</div>';
需要注意两个细节:
- 缩进会被保留 。上面例子中每行前的空格会原样进入字符串,如果对格式敏感(比如生成 SQL、比较字符串),可以用
trim()或专门的去缩进处理。 - 反引号需要转义 。字符串里出现 ````` 时写成 `````;出现
${时写成\${,否则会被当成插值。
3. 插值:${} 里可以放任何表达式
${} 中不仅能放变量,还能放任意 JavaScript 表达式:
javascript
const price = 99;
const count = 3;
console.log(`总价:${price * count} 元`); // 总价:297 元
console.log(`是否包邮:${price * count > 200 ? "是" : "否"}`); // 是否包邮:是
console.log(`大写:${"hello".toUpperCase()}`); // 大写:HELLO
甚至可以嵌套模板字符串:
javascript
const users = ["小明", "小红"];
const list = `用户列表:${users.map(u => `<li>${u}</li>`).join("")}`;
一个常见误区 :${} 里不要写语句(如 if、for),只能写表达式。需要复杂逻辑时,先在外部算好再插入。
4. 标签函数:把模板变成函数调用
这是模板字符串真正「进阶」的地方。在模板字符串前加一个函数名,就构成了标签模板:
javascript
function tag(strings, ...values) {
console.log(strings); // 静态字符串片段数组
console.log(values); // 插值结果数组
return "返回值";
}
const name = "小明";
const result = tag`你好,${name}!欢迎回来。`;
执行时,tag 收到的参数是:
strings:["你好,", "!欢迎回来。"]------ 被插值切开的静态片段values:["小明"]------ 每个${}求值后的结果
也就是说,strings.length === values.length + 1 恒成立。这个结构让标签函数可以完全掌控字符串的组装方式。
用一张图看清 tag 调用时字符串是如何被切分的:
4.1 strings.raw:拿到原始字符串
strings 数组还有一个 raw 属性,返回未处理转义的原始内容:
javascript
function rawTag(strings) {
console.log(strings[0]); // 实际字符(\n 变成换行)
console.log(strings.raw[0]); // 原始文本(保留 \n 两个字符)
}
rawTag`第一行\n第二行`;
这在需要保留源码原貌的场景(如代码高亮、模板引擎)中非常有用。
4.2 一个最小可用的标签函数
下面实现一个把插值全部转成大写的标签:
javascript
function upper(strings, ...values) {
return strings.reduce((acc, str, i) => {
const value = values[i] !== undefined ? String(values[i]).toUpperCase() : "";
return acc + str + value;
}, "");
}
const lang = "javascript";
console.log(upper`我正在学习 ${lang},感觉不错`);
// 我正在学习 JAVASCRIPT,感觉不错
reduce 配合 strings.length === values.length + 1 的特性,是标签函数最经典的写法模板。
5. 安全转义:防 XSS 的经典用法
标签模板最广为人知的应用,就是自动转义 HTML,防止 XSS 攻击。
普通拼接方式下,用户输入会被直接当作 HTML 解析:
javascript
const userInput = '<img src=x onerror="alert(1)">';
element.innerHTML = `<p>${userInput}</p>`; // 危险!脚本被执行
用标签函数做转义:
javascript
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function safeHtml(strings, ...values) {
return strings.reduce((acc, str, i) => {
const value = values[i] !== undefined ? escapeHtml(values[i]) : "";
return acc + str + value;
}, "");
}
const userInput = '<img src=x onerror="alert(1)">';
element.innerHTML = safeHtml`<p>${userInput}</p>`;
// 输出:<p><img src=x onerror="alert(1)"></p>,脚本不会执行
关键点 :静态片段 strings 是开发者自己写的,可信;只有 values 来自外部,需要转义。标签模板天然把「可信部分」和「不可信部分」分开了,这正是它比普通拼接更安全的原因。
整个转义过程的数据流如下:
注意:这只是原理演示。生产环境建议使用成熟的库(如
DOMPurify),并优先使用框架自带的转义机制。
6. 国际化与高亮:标签模板的进阶玩法
6.1 国际化(i18n)
标签模板可以让多语言文案的写法更自然:
javascript
const messages = {
zh: { greeting: "你好,{name}!你有 {count} 条新消息" },
en: { greeting: "Hello, {name}! You have {count} new messages" }
};
let currentLang = "zh";
function i18n(strings, ...values) {
const template = messages[currentLang].greeting;
let i = 0;
return template.replace(/\{(\w+)\}/g, () => values[i++]);
}
const name = "小明";
const count = 3;
console.log(i18n`你好,${name}!你有 ${count} 条新消息`);
// 切换语言后输出英文版本
currentLang = "en";
console.log(i18n`你好,${name}!你有 ${count} 条新消息`);
// Hello, 小明! You have 3 new messages
实际项目中,i18n 标签可以进一步结合 Intl 做数字、日期、货币的本地化格式化。
6.2 语法高亮
代码高亮库(如 lit-html、styled-components)大量使用标签模板。下面是一个极简的高亮示例:
javascript
function highlight(strings, ...values) {
const code = strings.reduce((acc, str, i) => {
return acc + str + (values[i] !== undefined ? values[i] : "");
}, "");
return code
.replace(/\b(const|let|var|function|return)\b/g, '<span class="kw">$1</span>')
.replace(/(["'`])(.*?)\1/g, '<span class="str">$1$2$1</span>');
}
const snippet = highlight`const msg = "hello";`;
console.log(snippet);
// <span class="kw">const</span> msg = <span class="str">"hello"</span>;
配合 CSS 给 .kw、.str 上色,就得到了一个可用的高亮器雏形。真实的高亮库会基于词法分析做得更精细,但核心思路一致:用标签函数接管字符串,再按规则重写。
6.3 其他常见应用
styled-components:styled.div\color: red;`` 把 CSS 写成标签模板,插值可动态计算。lit-html:用标签模板描述 DOM 结构,框架负责高效更新。- GraphQL 查询 :
gql\query { ... }`` 让编辑器识别语法并做校验。
7. 总结
| 特性 | 普通模板字符串 | 标签模板 |
|---|---|---|
| 多行支持 | ✅ | ✅ |
| 插值 | ✅ | ✅ |
| 自定义处理逻辑 | ❌ | ✅ |
| 自动转义 | ❌ | ✅(需自行实现) |
| 典型场景 | 日常拼接 | 安全转义、i18n、高亮、DSL |
一句话概括:模板字符串解决「怎么写」,标签模板解决「怎么处理」 。掌握标签函数后,你会发现很多看似神奇的库(styled-components、lit-html、gql)其实都建立在这个简单而强大的语言特性之上。
从能力维度看,标签模板的典型落地场景可以归纳为:
建议动手实现一遍本文的 safeHtml 和 highlight,理解 strings 与 values 的配对关系,这块知识就真正属于你了。