Tagged Templates: JavaScript's String Pipeline Beyond Concatenation
1. Introduction: Starting from + concatenation
Before ES6, string concatenation relied on +:
const name = "小明";
const age = 18;
const msg = "我叫" + name + ",今年" + age + "岁。";
That code works, but the problems are obvious: quotes, plus signs, and spaces mix together, becoming harder to read as it grows; newlines require \n; with many variables it becomes a nightmare.
ES6's Template Literals, wrapped in backticks (`), solve both the multi-line and interpolation pain points. Its advanced form, Tagged Templates, upgrades "string concatenation" into "programmable string processing", becoming the foundation for safe escaping, internationalization, syntax highlighting, and more.
This article goes from basic syntax to practical applications of tagged templates, helping you fully master this topic.
Here's an overview diagram to clarify the structure:
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. Multi-line strings: no more \n
The most obvious benefit of template literals is native multi-line support; you write what you get:
const html = `
<div class="card">
<h2>标题</h2>
<p>内容</p>
</div>
`;
Compared to the old way:
// 旧写法:需要手动拼接 \n,缩进也很别扭
var html = '<div class="card">\n' +
' <h2>标题</h2>\n' +
' <p>内容</p>\n' +
'</div>';
Two details to note:
- Indentation is preserved. Spaces before each line in the example become part of the string literally. If you care about formatting (e.g., generating SQL or comparing strings), you can use
trim()or a dedicated de-indent process. - Backticks need escaping. Write
\`for a backtick inside the string; write\${when a literal${should appear, otherwise it's treated as interpolation.
3. Interpolation: ${} can contain any expression
Inside ${} you can put not only variables but any JavaScript expression:
const price = 99;
const count = 3;
console.log(`总价:${price * count} 元`); // 总价:297 元
console.log(`是否包邮:${price * count > 200 ? "是" : "否"}`); // 是否包邮:是
console.log(`大写:${"hello".toUpperCase()}`); // 大写:HELLO
You can even nest template literals:
const users = ["小明", "小红"];
const list = `用户列表:${users.map(u => `<li>${u}</li>`).join("")}`;
A common misconception: ${} should not contain statements (like if, for), only expressions. When you need complex logic, compute it outside first and then insert it.
4. Tag functions: turning templates into function calls
This is where template literals really "advance". Placing a function name before a template literal creates a tagged template:
function tag(strings, ...values) {
console.log(strings); // 静态字符串片段数组
console.log(values); // 插值结果数组
return "返回值";
}
const name = "小明";
const result = tag`你好,${name}!欢迎回来。`;
When executed, tag receives these arguments:
strings:["你好,", "!欢迎回来。"]— the static fragments split by interpolationsvalues:["小明"]— the result of evaluating each${}
That is, strings.length === values.length + 1 always holds. This structure lets tag functions take complete control over how the string is assembled.
Use a diagram to see how the string is split when tag is called:
flowchart LR
T["tag`你好,${name}!欢迎回来。`"] --> S["strings 数组"]
T --> V["values 数组"]
S --> S1["strings[0] = 你好,"]
S --> S2["strings[1] = !欢迎回来。"]
V --> V1["values[0] = 小明"]
S1 --> R["按 strings.length = values.length + 1 重新组装"]
S2 --> R
V1 --> R
R --> O["最终返回字符串"]
4.1 strings.raw: getting the raw string
The strings array also has a raw property that returns the original content without escape processing:
function rawTag(strings) {
console.log(strings[0]); // 实际字符(\n 变成换行)
console.log(strings.raw[0]); // 原始文本(保留 \n 两个字符)
}
rawTag`第一行\n第二行`;
This is useful wherever you need to preserve source code appearance (e.g., code highlighting, template engines).
4.2 A minimal usable tag function
Here's a tag that uppercases all interpolated values:
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,感觉不错
Combining reduce with the strings.length === values.length + 1 invariant is the classic pattern for tag functions.
5. Safe escaping: the classic use case against XSS
The most widely known application of tagged templates is automatically escaping HTML to prevent XSS attacks.
With normal concatenation, user input is directly parsed as HTML:
const userInput = '<img src=x onerror="alert(1)">';
element.innerHTML = `<p>${userInput}</p>`; // 危险!脚本被执行
Escaping with a tag function:
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>,脚本不会执行
Key point: static fragments strings are written by the developer and are trusted; only values come from outside and need escaping. Tagged templates naturally separate the "trusted part" from the "untrusted part", which is why they're safer than plain concatenation.
The data flow of the whole escaping process:
flowchart TD
A["开发者编写的静态片段 strings"] --> C["safeHtml 标签函数"]
B["外部输入 values"] --> D["escapeHtml 转义"]
D --> C
C --> E["拼接为安全 HTML"]
E --> F["element.innerHTML 渲染"]
A -.->|"可信,无需处理"| C
B -.->|"不可信,必须转义"| D
Note: This is a principle demonstration. For production, use a mature library (like
DOMPurify) and prefer the framework's built-in escaping mechanism.
6. Internationalization and highlighting: advanced uses of tagged templates
6.1 Internationalization (i18n)
Tagged templates can make multi-language copy writing more natural:
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
In real projects, an i18n tag can be further combined with Intl for localized number, date, and currency formatting.
6.2 Syntax highlighting
Code highlighting libraries (like lit-html, styled-components) make heavy use of tagged templates. Here's a minimal highlighting example:
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>;
With CSS coloring .kw and .str, you get a usable highlighter prototype. Real highlighting libraries do more refined work based on lexical analysis, but the core idea is the same: use a tag function to take over the string, then rewrite it per rules.
6.3 Other common applications
styled-components:styled.div\color: red;`` writes CSS as tagged templates, with dynamic interpolation.lit-html: describes DOM structure with tagged templates; the framework handles efficient updates.- GraphQL queries:
gql\query { ... }`` lets editors recognize the syntax and validate it.
7. Summary
| Feature | Plain template literal | Tagged template |
|---|---|---|
| Multi-line support | ✅ | ✅ |
| Interpolation | ✅ | ✅ |
| Custom processing logic | ❌ | ✅ |
| Automatic escaping | ❌ | ✅ (must implement yourself) |
| Typical scenarios | daily concatenation | safe escaping, i18n, highlighting, DSLs |
In one sentence: template literals solve "how to write", tagged templates solve "how to process". After mastering tag functions, you'll find that many seemingly magical libraries (styled-components, lit-html, gql) are built on this simple yet powerful language feature.
From a capability perspective, typical applications of tagged templates can be summarized as:
mindmap
root(("标签模板应用场景"))
安全
"HTML 转义防 XSS"
"SQL 参数化"
展示
"语法高亮"
"CSS-in-JS"
内容
"国际化 i18n"
"GraphQL 查询"
工程
"lit-html 模板"
"自定义 DSL"
It's recommended to implement the safeHtml and highlight examples yourself to understand the pairing between strings and values; then this knowledge truly belongs to you.
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
The images are all blank, huh?
Thanks for pointing that out, I'll update it.