JavaScript 模板字符串与标签模板:不只是拼接

1. 引言:从 + 拼接说起

在 ES6 之前,我们拼接字符串基本靠 +:

javascript 复制代码
const name = "小明";
const age = 18;
const msg = "我叫" + name + ",今年" + age + "岁。";

这段代码能跑,但问题也很明显:引号、加号、空格混在一起,稍长一点就难以阅读;换行要写 \n;变量多了更是灾难。

ES6 引入的**模板字符串(Template Literals)**用反引号 ````` 包裹,一举解决了多行与插值两大痛点。而它的进阶形态------标签模板(Tagged Templates),则把「字符串拼接」升级成了「可编程的字符串处理」,成为安全转义、国际化、语法高亮等场景的底层基石。

本文从基础语法讲到标签模板的实战应用,帮你把这块内容彻底吃透。

先看一张全景图,理清本文的知识脉络:

flowchart TD A["模板字符串 Template Literals"] --> B["多行字符串"] A --> C["插值 ${}"] A --> D["标签模板 Tagged Templates"] D --> E["strings 静态片段"] D --> F["values 插值结果"] E --> G["安全转义 safeHtml"] F --> G D --> H["国际化 i18n"] D --> I["语法高亮 highlight"] D --> J["DSL:styled-components / lit-html / gql"]

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 调用时字符串是如何被切分的:

flowchart LR T[&#34;tag`你好,${name}!欢迎回来。`&#34;] --> S[&#34;strings 数组&#34;] T --> V[&#34;values 数组&#34;] S --> S1[&#34;strings[0] = 你好,&#34;] S --> S2[&#34;strings[1] = !欢迎回来。&#34;] V --> V1[&#34;values[0] = 小明&#34;] S1 --> R[&#34;按 strings.length = values.length + 1 重新组装&#34;] S2 --> R V1 --> R R --> O[&#34;最终返回字符串&#34;]

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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

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>&lt;img src=x onerror="alert(1)"&gt;</p>,脚本不会执行

关键点 :静态片段 strings 是开发者自己写的,可信;只有 values 来自外部,需要转义。标签模板天然把「可信部分」和「不可信部分」分开了,这正是它比普通拼接更安全的原因。

整个转义过程的数据流如下:

flowchart TD A[&#34;开发者编写的静态片段 strings&#34;] --> C[&#34;safeHtml 标签函数&#34;] B[&#34;外部输入 values&#34;] --> D[&#34;escapeHtml 转义&#34;] D --> C C --> E[&#34;拼接为安全 HTML&#34;] E --> F[&#34;element.innerHTML 渲染&#34;] A -.->|&#34;可信,无需处理&#34;| C B -.->|&#34;不可信,必须转义&#34;| D

注意:这只是原理演示。生产环境建议使用成熟的库(如 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)其实都建立在这个简单而强大的语言特性之上。

从能力维度看,标签模板的典型落地场景可以归纳为:

mindmap root((&#34;标签模板应用场景&#34;)) 安全 &#34;HTML 转义防 XSS&#34; &#34;SQL 参数化&#34; 展示 &#34;语法高亮&#34; &#34;CSS-in-JS&#34; 内容 &#34;国际化 i18n&#34; &#34;GraphQL 查询&#34; 工程 &#34;lit-html 模板&#34; &#34;自定义 DSL&#34;

建议动手实现一遍本文的 safeHtml 和 highlight,理解 strings 与 values 的配对关系,这块知识就真正属于你了。

相关推荐
恋猫de小郭41 分钟前
Flutter 开始支持 Jetpack Compose Platform View
android·前端·flutter
IT_陈寒1 小时前
React实战技巧总结
前端·人工智能·后端
陈随易8 小时前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
Dream-Y.ocean11 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn11 小时前
性能监控
前端
Highcharts.js12 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn12 小时前
构建工具
前端
FRing0312 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
kyriewen15 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员