夜雨聆风学习资料网

ARTICLE · 1164610

知识回顾24:模板字符串10道面试题,前端必问

知识回顾24:模板字符串10道面试题,前端必问

一、基础概念题

1、什么是模板字符串?核心特征有哪些?

一句话答案:模板字符串是 ES6 为 JS/TS 新增的字符串书写方式,用反引号()包裹,核心解决传统 + 拼接繁琐、可读性差的痛点;三大核心特征是反引号定界、${}插值嵌入变量/表达式、原生支持多行换行(不用手动加\n`),让字符串处理更简洁直观。
传统拼接的痛点:
const name = '张三'const age = 25// 传统拼接:引号、加号满天飞,变量多了容易漏引号错加号const str1 = '姓名:' + name + ',年龄:' + age + '岁'// 多行字符串:手动加 \n,可读性差const multi1 = '第一行\n第二行\n第三行'
模板字符串的写法:
// 模板字符串:反引号包裹,${} 插值,简洁直观const str2 = `姓名:${name},年龄:${age}岁`// 多行字符串:直接换行,书写格式和最终呈现一致const multi2 = `第一行第二行第三行`console.log(str1 === str2)        // trueconsole.log(multi1 === multi2)    // true
核心特征1:反引号定界
模板字符串不用单引号(')或双引号("),而是用反引号(`)包裹:
// 单引号字符串const a = 'hello'// 双引号字符串const b = "hello"// 模板字符串(反引号,键盘左上角 Esc 下方的符号)const c = `hello`
反引号的好处是:字符串内部可以自由使用单引号和双引号,不需要转义:
// 传统:内部用引号需要转义或换引号const str1 = '他说:"你好"'const str2 = "他说:'你好'"// 模板字符串:内部引号随便用const str3 = `他说:"你好",她说:'你好'`
核心特征2:${}插值嵌入
用 ${变量/表达式} 将变量、运算结果、函数返回值直接嵌入字符串,不用手动拼接:
const name = '张三'const age = 25// 变量插值const str = `姓名:${name},年龄:${age}岁`// 姓名:张三,年龄:25岁// 表达式插值const calc = `${a} + ${b} = ${a + b}`// 函数调用插值const greet = `欢迎:${getUserName()}`
${} 内可以放任意合法的 JS/TS 表达式,引擎会自动计算并把结果转成字符串插入。
核心特征3:原生多行换行
模板字符串内部可以直接换行,换行符会被保留在字符串中,不用手动加 \n:
// 传统:手动加 \nconst html1 = '<div>\n  <p>hello</p>\n</div>'// 模板字符串:直接换行,写起来就是渲染出来的样子const html2 = `<div>  <p>hello</p></div>`console.log(html1 === html2)  // true
这在拼接 HTML 模板、SQL 语句、多行文本时特别方便,代码可读性大大提升。
模板字符串 vs 传统字符串对比:
维度
传统字符串('或")
模板字符串(`)
定界符
单引号/双引号
反引号
变量插入
用 + 拼接
用 ${} 插值
多行字符串
手动加 \n
直接换行
内部引号
需转义或换引号
自由使用
表达式
拼接时计算
${} 内直接写
标签模板
不支持
支持(自定义解析逻辑)
一句话总结:模板字符串是 ES6 新增的字符串书写方式,用反引号()包裹,三大核心特征——反引号定界(内部可自由使用单双引号)、${}插值嵌入(变量/表达式/函数调用直接插入,不用 + 拼接)、原生多行换行(直接回车换行,不用手动加\n,书写格式即呈现格式);核心解决传统 +` 拼接繁琐、易出错、可读性差的痛点,是 JS/TS 开发中处理字符串的首选方式。

2、模板字符串的基本用法?插值语法怎么用?

一句话答案:模板字符串用反引号包裹,${表达式} 是插值语法——引擎会计算 ${} 内的表达式并把结果转成字符串插入对应位置;${} 内可以放变量、算术运算、比较运算、三元表达式、对象属性访问、数组方法、函数调用等任意合法表达式;最常用的场景是变量插值、多行字符串、表达式/函数调用插值。
用法1:变量插值(最常用)
const name = '张三'const age = 25const city = '北京'// 单个变量const str1 = `我叫${name}`// 我叫张三// 多个变量const str2 = `我叫${name},今年${age}岁,住在${city}`// 我叫张三,今年25岁,住在北京
用法2:多行字符串
// 直接换行,不用 \nconst address = `  姓名:张三  地址:北京市朝阳区  邮编:100000`// 拼接 HTML 模板(非常常见)const card = `  <div class="card">    <h3>${title}</h3>    <p>${content}</p>    <span>${date}</span>  </div>`
注意:模板字符串中的换行和缩进会被保留,包括开头和结尾的空白字符。如果不需要首尾空白,可以用 .trim() 去除。
用法3:表达式插值
${} 内不只是变量,还可以写任意表达式:
const a = 10const b = 20// 算术运算const calc = `${a} + ${b} = ${a + b},${a} × ${b} = ${a * b}`// 10 + 20 = 30,10 × 20 = 200// 比较运算const compare = `${a} > ${b} ? ${a > b}`// 10 > 20 ? false// 三元运算const level = `成绩:${score >= 60 ? '及格' : '不及格'}`// 成绩:及格// 逻辑运算const status = `状态:${isActive && isVerified ? '正常' : '异常'}`
用法4:对象属性访问
const user = {  name: '李四',  age: 30,  address: { city: '上海', district: '浦东新区' }}const info = `  姓名:${user.name}  年龄:${user.age}  城市:${user.address.city}  区域:${user.address.district}`
用法5:数组方法
const tags = ['前端', 'Vue', 'TypeScript']const tagStr = `标签:${tags.join('、')}`// 标签:前端、Vue、TypeScriptconst countStr = `共${tags.length}个标签,第一个是${tags[0]}`// 共3个标签,第一个是前端
用法6:函数调用
function formatDate(date) {  return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`}const now = new Date()const dateStr = `今天是:${formatDate(now)}`// 今天是:2026-10-7// 模板字符串中调用模板字符串(嵌套)const full = `用户信息:${`姓名:${name},年龄:${age}`}` 
插值的执行时机:
${} 内的表达式在模板字符串定义时立即执行,不是在使用时才执行:
let count = 0const str = `计数:${count++}`  // 定义时 count++ 就执行了console.log(count)  // 1(已经自增了)console.log(str)    // 计数:0(自增前的值)
这是一个重要的特性,也是一个常见的坑——插值有副作用时要小心。
插值结果的类型转换:
${} 内的表达式结果会被自动转成字符串:
`数字:${123}`        // "数字:123"`布尔:${true}`        // "布尔:true"`数组:${[1,2,3]}`     // "数组:1,2,3"(数组 toString)`对象:${{a:1}}`       // "对象:[object Object]"`null:${null}`        // "null:null"`undefined:${undefined}` // "undefined:undefined"
注意对象直接插值会显示 [object Object],需要用 JSON.stringify() 或访问具体属性。
一句话总结:模板字符串基本用法——用反引号包裹,${表达式} 插值(引擎计算表达式并转成字符串插入);${} 内可以放变量、算术/比较/逻辑运算、三元表达式、对象属性访问、数组方法、函数调用等任意合法表达式;最常用场景是变量插值(姓名:${name})、多行字符串(直接换行拼 HTML/SQL)、表达式插值(${a + b})、函数调用插值(${formatDate(now)});重要特性是插值在定义时立即执行(有副作用要小心),结果自动转字符串(对象需 JSON.stringify)。

3、模板字符串的底层原理是什么?拆解逻辑?

一句话答案:模板字符串本质是 ES6 的语法糖,JS 引擎解析时会把模板拆分为两个数组——字符串片段数组(TemplateStringsArray,按 ${} 切割的静态文本)和插值变量数组(${} 内表达式的计算结果),然后按顺序交替拼接生成最终字符串;这个拆分逻辑是标签模板(Tagged Templates)的核心基础,标签函数接收的第一个参数就是字符串片段数组,剩余参数是插值变量数组。
拆解过程演示:
const x = 10const y = 20const str = `数值1:${x},数值2:${y}`
引擎拆解:
模板:`数值1:${x},数值2:${y}`按 ${} 切割:  第一个 ${} 前面:"数值1:"  第一个 ${} 和第二个 ${} 之间:",数值2:"  第二个 ${} 后面:""(空字符串)字符串片段数组:["数值1:", ",数值2:", ""]插值变量数组:[10, 20]最终拼接:  "数值1:" + 10 + ",数值2:" + 20 + ""  = "数值1:10,数值2:20"
注意末尾空字符串:如果模板以 ${} 结尾,字符串片段数组最后会有一个空字符串 ""。这是因为切割后最后一个 ${} 后面没有文本了。
用标签函数验证拆解:
const x = 10const y = 20// 标签函数:第一个参数是字符串片段数组,剩余参数是插值变量function logTemplate(strings, ...values) {  console.log('字符串片段:', strings)  // ["数值1:", ",数值2:", ""]  console.log('插值变量:', values)  // [10, 20]  console.log('原始字符串(raw):', strings.raw)  // ["数值1:", ",数值2:", ""](raw 保留原始转义符)  // 手动拼接还原  return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '')}logTemplate`数值1:${x},数值2:${y}`
TemplateStringsArray 类型:
字符串片段数组不是普通数组,而是 TemplateStringsArray 类型,它有一个特殊的 raw 属性:
interface TemplateStringsArray extends ReadonlyArray<string> {  readonly raw: ReadonlyArray<string>}
  • strings[i]:处理后的字符串片段(转义符已解析,如 \n 变成换行)
  • strings.raw[i]:原始字符串片段(转义符保留,如 \n 还是字面量 \n)
raw 属性的作用:
function logRaw(strings, ...values) {  console.log('处理后:', strings[0])  // "路径:C:user\node"(\n 被解析为换行)  console.log('原始:', strings.raw[0]) // "路径:C:\\user\\node"(转义符保留)}logRaw`路径:C:\user\node`
String.raw 就是利用 strings.raw 实现的——它直接用 raw 数组拼接,不解析转义符。
编译后的 ES5 代码(近似):
模板字符串最终会被编译成普通的字符串拼接:
// ES6 模板字符串const str = `姓名:${name},年龄:${age}岁`// 编译后(近似)var str = "姓名:" + name + ",年龄:" + age + "岁"
标签模板编译后:
// ES6 标签模板tag`姓名:${name}`// 编译后(近似)tag(_templateObject, name)// _templateObject 是缓存的 TemplateStringsArray(["姓名:", ""])
为什么需要拆解成两个数组:
  • 标签模板的基础:标签函数需要分别拿到静态文本和动态值,才能自定义处理逻辑(如 XSS 过滤只处理动态值)
  • 性能优化:字符串片段数组是静态的,可以被缓存(同一个模板多次使用时不重复创建)
  • raw 属性:需要同时保留处理后和原始的字符串片段
一句话总结:模板字符串底层原理是语法糖——JS 引擎解析时按 ${} 把模板拆成两个数组,字符串片段数组(TemplateStringsArray,静态文本,含 raw 属性保留原始转义符)和插值变量数组(${} 表达式的计算结果),然后按顺序交替拼接生成最终字符串;如果模板以 ${} 结尾,字符串片段数组最后会有空字符串;这个拆解逻辑是标签模板的核心基础——标签函数的第一个参数就是字符串片段数组,剩余参数是插值变量数组;字符串片段数组是静态可缓存的,提升了重复使用的性能。

二、实操场景题

4、多行字符串和表达式/函数调用插值怎么用?实战场景?

一句话答案:多行字符串直接在反引号内换行即可(换行符被保留,不用手动加 \n),适合拼 HTML 模板、SQL 语句、多行文本;表达式插值在 ${} 内写算术/比较/三元/逻辑运算,函数调用插值在 ${} 内直接调用函数(含参数);这三个用法覆盖了 90% 的字符串拼接需求,是日常开发中最常用的场景。
多行字符串:告别\n拼接
// 传统方式:手动加 \n,可读性差const html1 = '<divclass="card">' +  '<h3>' + title + '</h3>' +  '<p>' + content + '</p>' +  '</div>'// 模板字符串:直接换行,写起来就是渲染出来的样子const html2 = `<divclass="card">  <h3>${title}</h3>  <p>${content}</p></div>`
多行字符串的注意事项:
换行和缩进会被保留:模板字符串中的所有空白字符(空格、换行、制表符)都会被保留在结果中
const str = `  hello`console.log(str)// 输出:// (空行)//   hello// (空行)// 开头和结尾的换行、中间的缩进都保留了
去除首尾空白:用 .trim() 或 .trimStart()/.trimEnd()
const str = `  hello`.trim()console.log(str)  // "hello"(首尾空白去掉了,中间缩进保留)
缩进对齐:多行字符串中的缩进是相对于模板字符串定义位置的,不是相对于变量作用域
实战场景1:拼接 HTML 模板
function renderUserCard(user) {  return `    <div class="user-card">      <img src="${user.avatar}" alt="${user.name}" />      <div class="user-info">        <h3>${user.name}</h3>        <p class="title">${user.title}</p>        <p class="desc">${user.description || '暂无简介'}</p>      </div>      <button class="follow-btn" data-id="${user.id}">        ${user.isFollowed ? '已关注' : '关注'}      </button>    </div>  `}
实战场景2:拼接 SQL 语句
function buildQuery(filters) {  return `    SELECT       u.id, u.name, u.email, u.created_at    FROM users u    WHERE 1=1      ${filters.name ? `AND u.name LIKE '%${filters.name}%'` : ''}      ${filters.status ? `AND u.status = '${filters.status}'` : ''}      ${filters.startDate ? `AND u.created_at >= '${filters.startDate}'` : ''}    ORDER BY u.created_at DESC    LIMIT ${filters.pageSize || 20}    OFFSET ${(filters.page - 1) * (filters.pageSize || 20)}  `}
注意:实际生产中 SQL 拼接要注意 SQL 注入,推荐用参数化查询而不是直接拼字符串。
实战场景3:生成错误提示/日志
function logError(error, context) {  const message = `========================================  错误时间:${newDate().toISOString()}  错误类型:${error.name}  错误信息:${error.message}  错误堆栈:${error.stack}  上下文:${JSON.stringify(context)}========================================  `  console.error(message)}
表达式插值:
const price = 99const count = 3const discount = 0.8const isVip = true// 算术运算const total = `总价:${price * count * discount}元`// 总价:237.6元// 三元运算const vipTag = `会员:${isVip ? 'VIP会员' : '普通用户'}`// 会员:VIP会员// 嵌套三元(多层判断)const level = `评级:${score >= 90 ? '优秀' : score >= 80 ? '良好' : score >= 60 ? '及格' : '不及格'}`// 逻辑运算const status = `状态:${isActive && isVerified ? '正常' : '待验证'}`// 比较运算const compare = `价格对比:原价${originalPrice}${originalPrice > currentPrice ? '>' : '<='} 现价${currentPrice}`
函数调用插值:
import dayjs from 'dayjs'const date = new Date()const amount = 1234567.89// 格式化日期const dateStr = `创建时间:${dayjs(date).format('YYYY-MM-DD HH:mm:ss')}`// 创建时间:2026-10-07 10:30:00// 格式化金额const moneyStr = `金额:¥${amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`// 金额:¥1,234,567.89// 编码 URLconst url = `链接:https://api.example.com/search?q=${encodeURIComponent(keyword)}`// 转义 HTMLconst safeHtml = `内容:${escapeHtml(userInput)}`
函数调用插值的嵌套:
// 函数返回对象,再访问属性const userStr = `用户名:${getUser().name}`// 数组方法链式调用const top3 = `前三名:${users.map(u => u.name).slice(0, 3).join('、')}`// 异步函数(注意:直接调用会显示 Promise 对象)const bad = `用户:${fetchUser()}`  // 用户:[object Promise]
实战场景4:动态生成正则表达式
function buildPattern(keyword) {  // 动态生成匹配关键词的正则  return new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')}
注意:模板字符串中 ${} 和正则的 $ 可能冲突,需要注意转义。
一句话总结:多行字符串直接在反引号内换行(换行缩进保留,用 .trim() 去首尾空白),适合拼 HTML 模板、SQL 语句、错误日志、多行文本;表达式插值在 ${} 内写算术/比较/三元/逻辑运算(${price * count}、${isVip ? 'VIP' : '普通'});函数调用插值在 ${} 内直接调用函数(${dayjs(date).format('YYYY-MM-DD')}、${amount.toLocaleString()});这三个用法覆盖 90% 字符串拼接需求;注意事项——多行字符串空白保留、SQL 拼接防注入、异步函数直接调用显示 Promise、正则中 $ 需转义。

5、String.raw 是什么?怎么用?什么场景需要原始字符串?

一句话答案:String.raw 是 JS 内置的标签函数,用于获取模板字符串的原始未转义形式——普通模板字符串会解析转义符(\n 变换行、\t 变制表符),String.raw 保留转义符的字面量(\n 还是 \n 两个字符);典型场景是处理 Windows 文件路径、正则表达式、需要保留反斜杠的字符串,避免转义符被意外解析。
普通模板字符串 vs String.raw:
// 普通模板字符串:解析转义符const normal = `路径:C:\user\node`console.log(normal)// 输出:// 路径:C:user// ode// \u 被尝试解析为 Unicode 转义(可能报错或忽略),\n 被解析为换行// String.raw:保留原始字符串(不解析转义符)const raw = String.raw`路径:C:\user\node`console.log(raw)// 输出:路径:C:\user\node// \u 和 \n 都保留为字面量字符,不被解析
String.raw 的本质:
String.raw 是一个内置的标签函数,它利用 TemplateStringsArray.raw 属性获取原始字符串片段,然后和插值变量拼接:
// String.raw 的简化实现function myRaw(strings, ...values) {  let result = ''  strings.forEach((str, i) => {    result += strings.raw[i]  // 用 raw 数组(原始未转义)    if (i < values.length) {      result += values[i]    }  })  return result}// 效果和 String.raw 一样console.log(myRaw`C:\user\node`)  // C:\user\node
核心区别:strings vs strings.raw
function compare(strings, ...values) {  console.log('处理后(strings):', strings[0])  // "C:user\node"(\n 被解析为换行)  console.log('原始(strings.raw):', strings.raw[0])  // "C:\\user\\node"(反斜杠保留,显示为 \\)}compare`C:\user\node`
  • strings[i]:转义符已被 JS 引擎解析(\n → 换行符)
  • strings.raw[i]:转义符保留为字面量(\n → 反斜杠 + n 两个字符)
场景1:Windows 文件路径
这是 String.raw 最常见的使用场景。Windows 路径用反斜杠分隔,普通字符串中反斜杠会被当作转义符:
// 普通字符串:反斜杠被转义,路径出错const path1 = 'C:\Users\Admin\Documents\file.txt'console.log(path1)// C:UsersAdminDocumentsfile.txt(反斜杠全没了)// 普通字符串:需要双反斜杠转义const path2 = 'C:\\Users\\Admin\\Documents\\file.txt'console.log(path2)// C:\Users\Admin\Documents\file.txt(正确,但写起来麻烦)// String.raw:直接写,不用转义const path3 = String.raw`C:\Users\Admin\Documents\file.txt`console.log(path3)// C:\Users\Admin\Documents\file.txt(正确,简洁)
场景2:正则表达式
正则表达式中经常用到反斜杠(如 \d、\w、\b),用 String.raw 可以避免双重转义:
// 普通字符串构造正则:需要双重转义(字符串转义 + 正则转义)const pattern1 = new RegExp('\\d{3}-\\d{4}-\\d{4}')// \\d 在字符串中变成 \d,正则再解析 \d 为数字// String.raw 构造正则:只需要正则层面的转义const pattern2 = new RegExp(String.raw`\d{3}-\d{4}-\d{4}`)// \d 直接保留,正则解析为数字// 两种方式等价,但 String.raw 更简洁console.log(pattern1.source === pattern2.source)  // true
场景3:包含大量转义符的字符串
// 普通字符串:每个反斜杠都要双写const escapeSeq1 = '换行符:\\n,制表符:\\t,回车:\\r,反斜杠:\\\\'// String.raw:直接写const escapeSeq2 = String.raw`换行符:\n,制表符:\t,回车:\r,反斜杠:\\`
场景4:代码生成/模板输出
生成代码字符串时,需要保留代码中的转义符:
function generateCode(variableName) {  return String.raw`    const ${variableName} = "hello\nworld";    console.log(${variableName});  `}// 生成的代码中 \n 保留为字面量,不会被解析成换行
String.raw 带插值的用法:
String.raw 和普通标签模板一样,支持 ${} 插值:
const dir = 'Users'const file = 'config.json'const path = String.raw`C:\${dir}\Admin\${file}`console.log(path)// C:\Users\Admin\config.json// 插值正常计算,静态部分的反斜杠保留
String.raw 的注意事项:
  • String.raw是标签函数,不是普通函数:调用方式是 String.raw\...`,不是 String.raw('...')`
  • 插值部分不受 raw 影响:${} 内的表达式正常计算,只有静态字符串部分保留原始转义
  • \ 结尾的问题:如果模板以单个 \ 结尾,可能会有问题(反斜杠转义了反引号),需要用 \\ 结尾
// 问题:\` 被解析为转义反引号,模板没结束// const bad = String.raw`路径:C:\`// 解决:用 \\ 结尾const good = String.raw`路径:C:\\`console.log(good)  // 路径:C:\
Unicode 转义:String.raw 也不会解析 \uXXXX Unicode 转义,会保留为字面量
和普通字符串的对比总结:
写法
反斜杠处理
适用场景
'C:\\user\\node'
需双反斜杠转义
简单路径
`C:\user\node`
转义符被解析(可能出错)
不适合路径
String.raw\
C:\user\node``
反斜杠保留
Windows路径、正则、代码生成
一句话总结:String.raw 是 JS 内置标签函数,获取模板字符串的原始未转义形式——普通模板字符串解析转义符(\n 变换行),String.raw 利用 TemplateStringsArray.raw 属性保留转义符字面量(\n 还是反斜杠+n);典型场景是 Windows 文件路径(不用双反斜杠转义)、正则表达式(避免双重转义)、代码生成(保留代码中的转义符)、含大量转义符的字符串;支持 ${} 插值(插值正常计算,静态部分保留原始);注意 String.raw 是标签函数用反引号调用不是普通函数,模板以 \ 结尾时需要用 \ 避免转义反引号。

6、标签模板(Tagged Templates)是什么?原理和应用场景?

一句话答案:标签模板是模板字符串的高级用法,在模板字符串前加一个标签函数(如 sanitizeHTML...),JS 引擎不会直接拼接字符串,而是把模板拆解成字符串片段数组和插值变量数组传给标签函数,由标签函数自定义处理逻辑(如 XSS 过滤、数据格式化、多语言解析);标签函数签名是 function tag(strings, ...values),第一个参数是 TemplateStringsArray,剩余参数是插值变量。
标签模板的基本语法:
// 普通模板字符串:直接拼接const str1 = `姓名:${name}`// 标签模板:在模板前加标签函数const str2 = myTag`姓名:${name}`// 等价于:myTag(["姓名:", ""], name)// 引擎把模板拆解后传给 myTag 函数
标签函数的参数:
function myTag(strings, ...values) {  // strings:字符串片段数组(TemplateStringsArray)  //   - 按 ${} 切割的静态文本  //   - 有 raw 属性(原始未转义字符串)  // values:插值变量数组(${} 内表达式的计算结果)  //   - values.length = strings.length - 1(通常)  console.log('strings:', strings)  // ["姓名:", ",年龄:", "岁"]  console.log('values:', values)    // ["张三", 25]  // 自定义处理逻辑,返回处理后的字符串  return strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '')}const name = '张三'const age = 25myTag`姓名:${name},年龄:${age}岁`
拆解过程:
模板:`姓名:${name},年龄:${age}岁`按 ${} 切割:  strings[0] = "姓名:"  ${name} → values[0] = "张三"  strings[1] = ",年龄:"  ${age} → values[1] = 25  strings[2] = "岁"调用标签函数:myTag(strings, "张三", 25)
应用场景1:XSS 过滤(最常用)
把用户输入的内容进行 HTML 转义,防止 XSS 攻击:
function sanitizeHTML(strings, ...values) {  let result = ''  strings.forEach((str, i) => {    result += str    if (values[i] !== undefined) {      // 对插值变量进行 HTML 转义      result += String(values[i])        .replace(/&/g, '&amp;')        .replace(/</g, '&lt;')        .replace(/>/g, '&gt;')        .replace(/"/g, '&quot;')        .replace(/'/g, '&#39;')    }  })  return result}// 使用:静态部分不转义,只有插值变量(用户输入)被转义const userInput = '<script>alert("恶意代码")</script>'const safeHTML = sanitizeHTML`<div>用户输入:${userInput}</div>`console.log(safeHTML)// <div>用户输入:&lt;script&gt;alert(&quot;恶意代码&quot;)&lt;/script&gt;</div>// 浏览器只当作文字显示,不会执行脚本
关键:标签模板只对 ${} 内的插值变量做处理,静态文本部分不处理。这正是 XSS 过滤需要的——模板中的 HTML 标签是开发者写的(可信),只有插值变量是用户输入的(不可信,需要转义)。
应用场景2:格式化金额/数字
function formatMoney(strings, ...values) {  return strings.reduce((acc, str, i) => {    const value = values[i]    // 数字类型的插值自动格式化为千分位金额    const formatted = typeof value === 'number'      ? `¥${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`      : value    return acc + str + (formatted !== undefined ? formatted : '')  }, '')}const price = 123456.789const fee = 987.6const result = formatMoney`订单金额:${price},手续费:${fee},总计:${price + fee}`console.log(result)// 订单金额:¥123,456.79,手续费:¥987.60,总计:¥124,444.39
应用场景3:多语言(i18n)
const i18n = {  zh: { greet: '你好', welcome: '欢迎使用{0}功能' },  en: { greet: 'Hello', welcome: 'Welcome to {0} feature' }}function t(lang) {  return (strings, ...values) => {    let result = strings.join('')    // 替换 {0}、{1} 占位符    values.forEach((val, i) => {      result = result.replace(`{${i}}`, val)    })    // 匹配多语言配置    return i18n[lang][result.trim()] || result  }}const zhT = t('zh')const enT = t('en')console.log(zhT`greet`)      // 你好console.log(enT`greet`)      // Hello
应用场景4:CSS-in-JS(如 styled-components)
流行的 CSS-in-JS 库(styled-components、emotion)大量使用标签模板:
import styled from 'styled-components'// 标签模板定义样式组件const Button = styled.button`  background: ${props => props.primary ? 'blue' : 'white'};  color: ${props => props.primary ? 'white' : 'blue'};  padding: 10px 20px;  border-radius: 4px;  cursor: pointer;`// 使用<Button primary>主要按钮</Button>
styled.button 是一个标签函数,接收 CSS 模板和插值,生成带样式的 React 组件。
应用场景5:SQL 查询构建(如 slonik)
import { sql } from 'slonik'// 标签模板安全构建 SQL(自动参数化,防 SQL 注入)const query = sql`  SELECT * FROM users  WHERE name = ${userName}  AND age > ${minAge}`// slonik 会自动把插值转为参数化查询,不会直接拼字符串
标签模板 vs 普通函数调用:
// 标签模板:语法简洁,静态和动态分离sanitizeHTML`<div>${userInput}</div>`// 普通函数调用:需要手动区分静态和动态sanitizeHTML('<div>' + userInput + '</div>')// 函数无法区分哪部分是静态的、哪部分是用户输入的,只能全部转义
标签模板的核心优势是:静态文本和动态值自动分离,标签函数能精确知道哪些是开发者写的(可信)、哪些是插值变量(需要处理)。
标签函数的返回值:
标签函数可以返回任意类型,不一定是字符串:
// 返回数组function toArray(strings, ...values) {  return values}const arr = toArray`a${1}b${2}c`  // [1, 2]// 返回对象function toObject(strings, ...values) {  return { strings, values }}
一句话总结:标签模板是模板字符串的高级用法,模板前加标签函数(tag...),引擎把模板拆解为字符串片段数组(TemplateStringsArray,含 raw 属性)和插值变量数组传给标签函数,由函数自定义处理逻辑;标签函数签名 function tag(strings, ...values),strings 是静态文本,values 是插值结果;核心优势是静态文本和动态值自动分离,能精确处理插值部分;典型应用——XSS 过滤(只转义插值变量)、金额/数字格式化、多语言 i18n、CSS-in-JS(styled-components)、SQL 安全构建(slonik 参数化);标签函数可以返回任意类型,不一定是字符串。

7、TS 模板字面量类型(Template Literal Types)是什么?怎么用?

一句话答案:模板字面量类型是 TypeScript 独有的高级类型特性(JS 没有),用反引号和 ${} 在类型层面拼接字符串类型,能基于已有字符串字面量类型组合生成新的联合类型或精确约束字符串格式;典型用法是类型拼接(${Color}-${Size} 生成所有组合)、结合内置工具类型转换大小写(Uppercase/Lowercase)、约束 API 路径/事件名/CSS 类名等字符串格式,在编译期就能捕获拼写错误。
基础:字符串字面量类型
在了解模板字面量类型之前,先回顾字符串字面量类型:
// 字符串字面量类型:变量只能是这几个值之一type Color = 'red' | 'blue' | 'green'type Size = 'small' | 'large'const c: Color = 'red'    // 合法// const c2: Color = 'yellow'  // 报错:类型不匹配
模板字面量类型:类型层面的字符串拼接
用反引号和 ${} 在类型层面拼接:
type Color = 'red' | 'blue'type Size = 'small' | 'large'// 模板字面量类型:组合出所有可能的字符串type Style = `${Color}-${Size}`// Style = "red-small" | "red-large" | "blue-small" | "blue-large"const s1: Style = 'red-small'   // 合法const s2: Style = 'blue-large'  // 合法// const s3: Style = 'green-small' // 报错:green 不在 Color 中// const s4: Style = 'red-medium'  // 报错:medium 不在 Size 中
原理:TS 编译器会对联合类型做笛卡尔积,生成所有可能的组合。Color 有 2 个值,Size 有 2 个值,组合出 2×2=4 个类型。
结合内置工具类型:大小写转换
TS 提供了四个内置的字符串操作工具类型:
type Color = 'red' | 'blue'// Uppercase<T>:转大写type UpperColor = `${Uppercase<Color>}`// "RED" | "BLUE"// Lowercase<T>:转小写type LowerColor = `${Lowercase<'RED' | 'BLUE'>}`// "red" | "blue"// Capitalize<T>:首字母大写type CapColor = `${Capitalize<'red' | 'blue'>}`// "Red" | "Blue"// Uncapitalize<T>:首字母小写type UncapColor = `${Uncapitalize<'Red' | 'Blue'>}`// "red" | "blue"
实战场景1:约束 API 路径
type ApiModule = 'user' | 'order' | 'product'type ApiAction = 'list' | 'detail' | 'create' | 'update' | 'delete'// 约束 API 路径格式:/模块/操作type ApiPath = `/${ApiModule}/${ApiAction}`// "/user/list" | "/user/detail" | ... | "/product/delete"// 共 3×5 = 15 种可能的路径function request(path: ApiPath) {  console.log('请求:', path)}request('/user/list')      // 合法request('/order/detail')   // 合法// request('/goods/list')   // 报错:goods 不在 ApiModule 中// request('/user/export')   // 报错:export 不在 ApiAction 中// request('user/list')      // 报错:缺少开头的 /
实战场景2:约束事件名
type EventName = 'click' | 'change' | 'input' | 'submit'type EventHandler = `on${Capitalize<EventName>}`// "onClick" | "onChange" | "onInput" | "onSubmit"interface Props {  onClick?: () => void  onChange?: (value: string) => void  // 可以用 EventHandler 约束所有事件处理函数  [key: `on${Capitalize<EventName>}`]: ((...args: any[]) => void) | undefined}
实战场景3:约束 CSS 类名
type ButtonVariant = 'primary' | 'secondary' | 'danger'type ButtonSize = 'sm' | 'md' | 'lg'type ButtonClass = `btn-${ButtonVariant}-${ButtonSize}`// "btn-primary-sm" | "btn-primary-md" | ... | "btn-danger-lg"function getButtonClass(variant: ButtonVariant, size: ButtonSize): ButtonClass {  return `btn-${variant}-${size}`  // TS 能推断返回类型是 ButtonClass}
实战场景4:对象属性名约束
type UserField = 'name' | 'email' | 'phone'// 约束 get/set 方法名type UserGetter = `get${Capitalize<UserField>}`// "getName" | "getEmail" | "getPhone"type UserSetter = `set${Capitalize<UserField>}`// "setName" | "setEmail" | "setPhone"// 约束带前缀的属性type UserState = {  [K in UserField as `user_${K}`]: string}// { user_name: string; user_email: string; user_phone: string }
实战场景5:Vue/React Props 约束
// Vue 组件的 v-model 事件名约束type ModelEvent = `update:${string}`// 约束以 "update:" 开头的事件名// React 的 data-* 属性interface DataAttributes {  [key: `data-${string}`]: string}// 约束所有以 "data-" 开头的属性
模板字面量类型的推断:
TS 能从模板字面量类型中推断出具体类型:
function greet<T extends string>(name: T): `Hello, ${T}!` {  return `Hello, ${name}!`}const result = greet('World')// result 的类型被推断为 "Hello, World!"(字面量类型,不是 string)
注意事项:
联合类型数量爆炸:如果联合类型的值很多,笛卡尔积会产生大量类型,可能影响编译性能
type A = 'a' | 'b' | 'c' | 'd' | 'e'  // 5个type B = '1' | '2' | '3' | '4' | '5'  // 5个type C = `${A}-${B}`  // 25个类型,还能接受// 如果 A 和 B 各有 100 个,就是 10000 个类型,编译会很慢
只能用字符串字面量类型:${} 内必须是字符串字面量类型或能推断为字符串的类型,不能用 number 等
type N = numbertype Bad = `value-${N}`  // 报错:number 不能用于模板字面量类型type Good = `value-${string}`  // 合法(string 是宽泛类型)
TS 4.1+ 才支持:模板字面量类型是 TS 4.1 引入的,低版本不支持
和 JS 模板字符串的区别:
维度
JS 模板字符串
TS 模板字面量类型
层面
运行时(值层面)
编译时(类型层面)
作用
拼接字符串值
约束/生成字符串类型
${}内
变量/表达式(运行时计算)
类型(编译时推断)
结果
字符串值
字符串字面量类型/联合类型
JS 支持
是
否(TS 独有)
一句话总结:模板字面量类型是 TS 独有的高级类型特性(TS 4.1+),在类型层面用反引号和 ${} 拼接字符串字面量类型,对联合类型做笛卡尔积生成所有组合;核心用法——类型拼接(${Color}-${Size} 生成所有组合)、结合 Uppercase/Lowercase/Capitalize/Uncapitalize 转换大小写、约束 API 路径(/${Module}/${Action})、事件名(on${Capitalize})、CSS 类名(btn-${Variant}-${Size})、对象属性名(data-${string});在编译期捕获拼写错误,提升代码安全性;注意联合类型过多会导致类型数量爆炸影响编译性能,${} 内只能用字符串类型,这是 TS 独有特性 JS 没有。

三、深度理解题

8、插值的任意表达式能力有哪些?异步插值要注意什么?

一句话答案:${} 内可以放任意合法的 JS/TS 表达式——变量、算术/比较/逻辑运算、三元表达式(含嵌套三元)、对象属性访问、数组方法(含链式调用)、函数调用(含参数)、甚至模板字符串嵌套;但插值是同步立即执行的,直接放异步函数会显示 [object Promise],正确做法是在外层包 async 函数用 await 拿到结果后再插值,或先把异步结果存到变量再插值。
插值支持的表达式类型:
1. 变量
const name = '张三'`姓名:${name}`  // 姓名:张三
2. 算术运算
const a = 10, b = 20`和:${a + b},积:${a * b},商:${(a / b).toFixed(2)}`// 和:30,积:200,商:0.50
3. 比较运算
`${a} > ${b} ? ${a > b}`// 10 > 20 ? false
4. 逻辑运算
const isActive = true, isVerified = false`状态:${isActive && isVerified ? '正常' : '异常'}`// 状态:异常
5. 三元表达式(含嵌套)
const score = 85`评级:${score >= 90 ? '优秀' : score >= 80 ? '良好' : score >= 60 ? '及格' : '不及格'}`// 评级:良好
6. 对象属性访问(含嵌套)
const user = { name: '李四', address: { city: '上海' } }`城市:${user.address.city}`// 城市:上海
7. 数组方法(含链式调用)
const users = [{ name: '张三', score: 90 }, { name: '李四', score: 80 }]`前三名:${users.map(u => u.name).join('、')}`// 前三名:张三、李四`平均分:${(users.reduce((sum, u) => sum + u.score, 0) / users.length).toFixed(1)}`// 平均分:85.
8. 函数调用(含参数)
function formatDate(date) {  return date.toISOString().slice(0, 10)}`日期:${formatDate(newDate())}`// 日期:2026-10-07
9. 模板字符串嵌套
const name = '张三'`用户信息:${`姓名:${name}`}`// 用户信息:姓名:张三
10. 可选链和空值合并
const user = null`姓名:${user?.name ?? '未知'}`// 姓名:未知
复杂表达式实战(TS 类型推导):
interface User {  name: string  tags: string[]  score: number}const user: User = { name: '李四', tags: ['前端', 'TS'], score: 85 }const userInfo = `  用户名:${user.name}  标签:${user.tags.join('、') || '无标签'}  评级:${user.score >= 90 ? '优秀' : user.score >= 80 ? '良好' : '及格'}  加分后:${user.score + user.tags.length * 5}`
异步插值的问题:
${} 内的表达式是同步立即执行的,如果放异步函数,会直接把 Promise 对象转成字符串:
async function getUserId(name) {  // 模拟接口请求  return new Promise(resolve => setTimeout(() => resolve(1001), 100))}// 问题:直接调用异步函数,显示 Promise 对象const badStr = `用户ID:${getUserId('李四')}`console.log(badStr)// 用户ID:[object Promise]
原因:getUserId('李四') 返回的是 Promise 对象,${} 把 Promise 对象转成字符串就是 [object Promise]。异步结果还没返回,插值已经执行完了。
正确做法1:外层包 async 函数,await 后再插值
async function renderUserInfo(name) {  const userId = await getUserId(name)  // 先 await 拿到结果  const str = `用户ID:${userId}`        // 再用结果插值  console.log(str)  // 用户ID:1001  return str}renderUserInfo('李四')
正确做法2:先把异步结果存到变量,再插值
// 在 Vue/React 组件中const userId = ref(null)async function loadUser() {  userId.value = await getUserId('李四')}// 模板中用变量插值(变量有值后才渲染)// <div>用户ID:{{ userId }}</div> 
正确做法3:用 IIFE(立即执行函数)
// 不推荐,可读性差const str = (async () => {  const userId = await getUserId('李四')  return `用户ID:${userId}`})()// str 还是 Promise,需要 await  
异步插值的注意事项:
  • ${}是同步的:插值在模板字符串定义时同步执行,不会等待异步结果
  • Promise 转字符串是[object Promise]:直接放异步函数会显示这个,不是你想要的结果
  • 先 await 再插值:在 async 函数中用 await 拿到结果,再用结果插值
  • 组件中用响应式变量:Vue/React 中先把异步结果存到 ref/state,模板中用变量插值
  • 不要在${}中放有副作用的异步操作:如发请求、修改全局状态
插值的副作用问题:
${} 内的表达式会立即执行,如果表达式有副作用,会产生意外效果:
let count = 0// 副作用:count++ 会立即执行const str = `计数:${count++}`console.log(count)  // 1(已经自增了)console.log(str)    // 计数:0(自增前的值,因为 count++ 是后自增)// 更危险:函数调用有副作用function increment() {  count++  return count}const str2 = `计数:${increment()}`console.log(count)  // 2
建议:${} 内尽量放纯表达式(无副作用),有副作用的操作放在模板字符串外面执行。
插值的执行次数:
模板字符串定义时插值执行一次,之后变量变化不会自动更新:
let name = '张三'const str = `姓名:${name}`name = '李四'console.log(str)  // 姓名:张三(不会自动更新)
如果需要动态更新,封装为函数:
const render = (name) => `姓名:${name}`console.log(render('张三'))  // 姓名:张三console.log(render('李四'))  // 姓名:李四
一句话总结:${} 内支持任意合法 JS/TS 表达式——变量、算术/比较/逻辑运算、三元(含嵌套)、对象属性访问(含嵌套)、数组方法(含链式)、函数调用(含参数)、模板字符串嵌套、可选链/空值合并;但插值是同步立即执行的,直接放异步函数会显示 [object Promise](Promise 转字符串的结果),正确做法是在 async 函数中先 await 拿到结果再插值,或组件中把异步结果存到 ref/state 再插值;另外 ${} 内表达式有副作用时会立即执行(如 count++),建议放纯表达式;模板字符串定义时插值执行一次,之后变量变化不自动更新,需要动态渲染时封装为函数。

9、标签模板有哪些高级应用?金额格式化、多语言、模板编译怎么实现?

一句话答案:标签模板的高级应用核心是利用"静态文本和动态值自动分离"的特性自定义处理逻辑——金额格式化标签函数自动识别数字插值并格式化为千分位货币、多语言标签函数根据语言配置替换占位符、模板编译把模板字符串预编译为渲染函数避免重复解析提升性能;这些应用的本质都是在标签函数中对 strings 和 values 做自定义拼接和处理,返回处理后的结果。
高级应用1:金额/数字格式化
标签函数自动识别数字类型的插值,格式化为千分位货币:
function formatMoney(strings, ...values) {  return strings.reduce((acc, str, i) => {    const value = values[i]    // 只对数字类型的插值做格式化    const formatted = typeof value === 'number'      ? `¥${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`      : value    return acc + str + (formatted !== undefined ? formatted : '')  }, '')}// 使用const price = 123456.789const fee = 987.6const result = formatMoney`订单金额:${price},手续费:${fee},总计:${price + fee}`console.log(result)// 订单金额:¥123,456.79,手续费:¥987.60,总计:¥124,444.39
关键点:
  • 用 typeof value === 'number' 判断是否是数字
  • toFixed(2)保留两位小数
  • 正则 /\B(?=(\d{3})+(?!\d))/g 实现千分位逗号
  • 非数字插值原样返回
扩展:支持多种格式化选项
function money(options = {}) {  const { prefix = '¥', decimals = 2, thousands = ',' } = options  return (strings, ...values) => {    return strings.reduce((acc, str, i) => {      const value = values[i]      const formatted = typeof value === 'number'        ? `${prefix}${value.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, thousands)}`        : value      return acc + str + (formatted !== undefined ? formatted : '')    }, '')  }}// 美元格式const usd = money({ prefix: '$', decimals: 2 })console.log(usd`价格:${99.9}`)  // 价格:$99.90// 人民币格式const cny = money({ prefix: '¥', decimals: 2 })console.log(cny`价格:${99.9}`)  // 价格:¥99.90
高级应用2:多语言(i18n)
标签函数实现多语言模板解析:
// 多语言配置const i18n = {  zh: {    greet: '你好',    welcome: '欢迎使用{0}功能',    orderCount: '您有{0}个订单,其中{1}个待处理'  },  en: {    greet: 'Hello',    welcome: 'Welcome to {0} feature',    orderCount: 'You have {0} orders, {1} pending'  }}// 标签函数工厂:根据语言返回标签函数function t(lang) {  return (strings, ...values) => {    // 把字符串片段拼接成 key    const key = strings.join('').trim()    // 从多语言配置中查找翻译    let template = i18n[lang][key] || key    // 替换 {0}、{1} 等占位符    values.forEach((val, i) => {      template = template.replace(`{${i}}`, val)    })    return template  }}// 使用const zh = t('zh')const en = t('en')console.log(zh`greet`)        // 你好console.log(en`greet`)        // Helloconsole.log(zh`welcome`('模板字符串'))  // 欢迎使用模板字符串功能console.log(zh`orderCount`(5, 2))       // 您有5个订单,其中2个待处理
关键点:
  • strings.join('')把静态片段拼接成 key(因为没有 ${} 时 strings 只有一个元素)
  • 从多语言配置查找翻译模板
  • 用 {0}、{1} 占位符替换插值变量
  • 用柯里化(t(lang) 返回标签函数)支持语言切换
高级应用3:模板编译(性能优化)
把模板字符串预编译为渲染函数,避免重复解析提升性能:
function compileTemplate(template) {  // 1. 提取插值变量名:匹配 ${变量名}  const varMatches = template.match(/\$\{(\w+)\}/g) || []  const varNames = varMatches.map(v => v.slice(2, -1))  // 去掉 ${ 和 }  // 2. 生成函数体:用模板字符串拼接  const funcBody = `return \`${template}\`;`  // 3. 动态创建函数(参数是提取的变量名,函数体是模板拼接)  return new Function(...varNames, funcBody)}// 使用:编译一次,重复调用const userTemplate = compileTemplate(`姓名:${name},年龄:${age}岁`)// 第一次渲染console.log(userTemplate('张三', 25))  // 姓名:张三,年龄:25岁// 第二次渲染(无需重新解析模板,直接调用函数)console.log(userTemplate('李四', 30))  // 姓名:李四,年龄:30岁
性能提升原理:
不编译:每次渲染都要解析模板字符串 → 拆解 strings/values → 拼接  渲染1000次 = 解析1000次 + 拼接1000次编译后:只解析一次生成函数 → 每次渲染直接调用函数  渲染1000次 = 解析1次 + 函数调用1000次(函数调用比解析快得多)
安全注意:new Function 有安全风险,不要用用户输入的模板,只用开发者写的固定模板。
高级应用4:安全 HTML 渲染(类似 lit-html)
// 标签函数:自动转义插值,安全渲染 HTMLfunction html(strings, ...values) {  let result = ''  strings.forEach((str, i) => {    result += str    if (values[i] !== undefined) {      // 如果是数组(嵌套 html 调用),递归处理      if (Array.isArray(values[i])) {        result += values[i].join('')      } else {        // 转义 HTML 特殊字符        result += String(values[i])          .replace(/&/g, '&amp;')          .replace(/</g, '&lt;')          .replace(/>/g, '&gt;')          .replace(/"/g, '&quot;')      }    }  })  return result}// 使用const userInput = '<script>alert("xss")</script>'const safeHTML = html`<div>  <p>用户名:${userInput}</p>  <p>年龄:${25}</p></div>`// 插值变量被转义,静态 HTML 标签保留
高级应用5:SQL 参数化构建
// 标签函数:安全构建 SQL,自动参数化function sql(strings, ...values) {  let query = ''  const params = []  strings.forEach((str, i) => {    query += str    if (values[i] !== undefined) {      params.push(values[i])      query += `$${params.length}`  // 用占位符代替直接拼接    }  })  return { query, params }}// 使用const name = '张三'const age = 25const { query, params } = sql`SELECT * FROM users WHERE name = ${name} AND age > ${age}`console.log(query)   // SELECT * FROM users WHERE name = $1 AND age > $2console.log(params)  // ['张三', 25]// 自动参数化,防止 SQL 注入
标签模板高级应用的共同模式:
  • 接收 strings 和 values:所有标签函数都是这个签名
  • 遍历拼接:用 reduce 或 forEach 交替拼接 strings[i] 和 values[i]
  • 自定义处理 values:对插值变量做特殊处理(转义、格式化、参数化)
  • 返回处理结果:通常是字符串,也可以是对象/数组
一句话总结:标签模板高级应用利用"静态文本和动态值自动分离"特性自定义处理逻辑——金额格式化(判断数字类型,toFixed+正则千分位,支持柯里化配置前缀/小数位)、多语言 i18n(strings 拼接成 key 查翻译表,{0}占位符替换插值,柯里化切换语言)、模板编译(提取变量名+new Function 生成渲染函数,避免重复解析提升性能,注意安全)、安全 HTML 渲染(插值自动 HTML 转义,静态标签保留)、SQL 参数化(插值替换为 $1/$2 占位符,返回 query+params 防注入);共同模式是接收 strings+values → 遍历交替拼接 → 自定义处理 values → 返回结果,本质都是在标签函数中实现自定义的字符串处理逻辑。

10、模板字符串有哪些常见坑?怎么避坑?

一句话答案:模板字符串六大常见坑——反引号需要转义(模板内显示反引号用 `)、插值有副作用(${} 内表达式立即执行,count++会自增)、不是动态模板(插值在定义时执行,变量后续变化不自动更新,需封装函数)、异步插值显示[object Promise](需先 await 再插值)、多行字符串空白保留(首尾换行缩进都保留,用 .trim()去除)、对象插值显示[object Object](需 JSON.stringify 或访问具体属性)。
坑1:反引号的转义问题
模板字符串内部需要显示反引号时,必须用 \ 转义:
// 问题:内部反引号会被当作模板结束符// const str = `反引号:`,插值:${100}`  // 语法错误// 解决:用 \ 转义反引号const str = `反引号:\`,插值:${100}`console.log(str)  // 反引号:`,插值:100
同样,${ 序列在模板中会被当作插值开始,如果需要显示字面量 ${,也要转义:
// 显示字面量 ${const str = `显示 \${ 字面量`console.log(str)  // 显示 ${ 字面量
坑2:插值中的副作用
${} 内的表达式会立即执行,如果表达式有副作用(修改变量、调用有副作用的函数),会产生意外效果:
let count = 0// 问题:count++ 会立即自增const badStr = `计数:${count++}`console.log(count)  // 1(已经自增了)console.log(badStr) // 计数:0(后自增返回的是旧值)// 更隐蔽的问题:函数调用有副作用function incrementAndReturn() {  count++  return count}const str2 = `计数:${incrementAndReturn()}`console.log(count)  // 2
解决:
// 1. 插值中用纯表达式,无副作用const goodStr = `计数:${count}`// 2. 有副作用的操作放在模板外面count++const str = `计数:${count}`
坑3:模板字符串不是"动态模板"
模板字符串的插值在定义时执行,不是在使用/调用时。变量后续变化不会自动更新模板字符串:
let name = '张三'// 问题:定义时插值已固定为"张三"const badTemplate = `姓名:${name}`name = '李四'  // 修改变量console.log(badTemplate)  // 姓名:张三(不会自动更新为李四)
原因:模板字符串在定义时就已经把 ${name} 计算成具体值并拼接成最终字符串了,之后 name 变化和这个字符串没有关系。
解决:封装为函数,调用时插值
// 正确:封装为函数,每次调用时重新插值const renderName = (name) => `姓名:${name}`console.log(renderName('张三'))  // 姓名:张三console.log(renderName('李四'))  // 姓名:李四(调用时用新值插值)
在 Vue/React 中:模板({{ name }})是响应式的,因为框架会在数据变化时重新渲染模板。但 JS 中的模板字符串(姓名:${name})不是响应式的,定义时就固定了。
坑4:异步插值显示[object Promise]
${} 内直接放异步函数,会显示 Promise 对象的字符串形式:
async function getData() {  return new Promise(resolve => setTimeout(() => resolve('数据'), 100))}// 问题:直接调用异步函数const badStr = `结果:${getData()}`console.log(badStr)  // 结果:[object Promise]
原因:getData() 返回的是 Promise 对象,${} 把 Promise 转成字符串就是 [object Promise]。异步结果还没返回,插值已经执行完了。
解决:
// 1. 先 await 拿到结果,再插值(在 async 函数中)async function render() {  const data = await getData()  const str = `结果:${data}`  console.log(str)  // 结果:数据}// 2. 先把结果存到变量,再插值(组件中)const data = ref(null)onMounted(async () => {  data.value = await getData()})// 模板中用 {{ data }}
坑5:多行字符串的空白字符保留
模板字符串中的所有空白字符(换行、空格、缩进)都会被保留在结果中:
const str = `  hello  world`console.log(JSON.stringify(str))// "\n  hello\n  world\n"// 开头有换行,每行有缩进,结尾有换行
这在某些场景下会导致问题:
  • 字符串比较时多了空白字符
  • 发送给后端时多了不必要的空白
  • 显示时格式不符合预期
解决:
// 1. 用 .trim() 去除首尾空白const str1 = `  hello`.trim()console.log(JSON.stringify(str1))  // "hello"(首尾空白去掉了,中间缩进保留)// 2. 用 .trimStart() / .trimEnd() 只去一端const str2 = `  hello`.trimStart()// 3. 不在模板中写缩进(但可读性差)const str3 = `helloworld`
坑6:对象插值显示[object Object]
直接把对象放入 ${},会显示 [object Object]:
const user = { name: '张三', age: 25 }// 问题:对象直接插值const badStr = `用户:${user}`console.log(badStr)  // 用户:[object Object]
原因:对象转字符串时调用 toString() 方法,默认返回 [object Object]。
解决:
// 1. 访问具体属性const str1 = `用户:${user.name},年龄:${user.age}`// 用户:张三,年龄:25// 2. 用 JSON.stringify 转成 JSON 字符串const str2 = `用户:${JSON.stringify(user)}`// 用户:{"name":"张三","age":25}// 3. 给对象自定义 toString 方法const user2 = {  name: '张三',  age: 25,  toString() {    return `${this.name}(${this.age}岁)`  }}const str3 = `用户:${user2}`// 用户:张三(25岁)
坑7:模板字符串中的 this 指向
模板字符串中的 this 指向和普通字符串一样,取决于函数调用方式:
const obj = {  name: '张三',  greet: function() {    return `你好,${this.name}`  // this 指向 obj  },  greetArrow: () => {    return `你好,${this.name}`  // this 指向外层(可能是 undefined 或全局)  }}console.log(obj.greet())       // 你好,张三console.log(obj.greetArrow())  // 你好,undefined(箭头函数的 this 是外层的)
坑8:正则表达式中的$冲突
模板字符串中 ${ 是插值语法,和正则中的 $ 可能冲突:
// 问题:正则中的 ${ 被当作插值// const pattern = new RegExp(`\d${2}`)  // 语法错误或意外插值// 解决:用 \\$ 转义,或用 String.rawconst pattern1 = new RegExp(`\\d\${2}`)const pattern2 = new RegExp(String.raw`\d${2}`)  // String.raw 中 ${ 还是插值// 注意:String.raw 也会解析 ${} 插值,只是不解析转义符
避坑总结表:
坑
表现
解决
反引号转义
内部反引号语法错误
用 ``` 转义
插值副作用
count++
 意外自增
插值用纯表达式,副作用放外面
不是动态模板
变量变化不更新
封装为函数,调用时插值
异步插值
显示 [object Promise]
先 await 再插值
空白保留
首尾多了换行缩进
用 .trim() 去除
对象插值
显示 [object Object]
访问属性或 JSON.stringify
this 指向
箭头函数 this 不对
普通函数 this 指向调用者
正则 $ 冲突
${
 被当作插值
用 \\$ 转义
一句话总结:模板字符串六大常见坑——反引号需转义(内部显示反引号用 `,显示 ${用${)、插值有副作用(${} 内表达式立即执行,count++会自增,建议用纯表达式)、不是动态模板(插值在定义时执行,变量后续变化不自动更新,需封装为函数调用时插值)、异步插值显示[object Promise](需在 async 函数中先 await 拿到结果再插值)、多行字符串空白保留(首尾换行缩进都保留,用 .trim()/.trimStart()去除)、对象插值显示[object Object](需访问具体属性或 JSON.stringify 或自定义 toString);另外注意箭头函数中 this 指向、正则中 $和${ 冲突——记住这些坑,模板字符串就能用得又快又稳。

四、回顾总结

10道题做完,感觉怎么样?
模板字符串是 ES6 最实用的语法糖之一,日常开发中几乎每行代码都在用。核心要掌握:三大核心特征(反引号定界、${} 插值、原生多行)、基本用法(变量插值、多行字符串、表达式/函数调用)、底层原理(拆解为字符串片段数组+插值变量数组,TemplateStringsArray 含 raw 属性)、String.raw 原始字符串(保留转义符,Windows路径/正则场景)、标签模板 Tagged Templates(自定义处理逻辑,XSS过滤/金额格式化/多语言/CSS-in-JS/SQL构建)、TS 模板字面量类型(类型层面拼接,约束API路径/事件名/CSS类名)、插值任意表达式(三元/数组方法/函数调用,注意异步和副作用)、标签模板高级应用(金额格式化/多语言/模板编译性能优化)、六大常见坑(反引号转义/副作用/非动态模板/异步Promise/空白保留/对象[object Object])。把这些搞清楚,字符串处理就不会再用繁琐的 + 拼接了。
核心知识点回顾:
模块
关键记忆点
核心特征
反引号定界、${}插值嵌入、原生多行换行
基本用法
变量插值、多行字符串、表达式/函数调用插值,插值在定义时立即执行
底层原理
拆解为字符串片段数组(TemplateStringsArray含raw)+插值变量数组,交替拼接
String.raw
内置标签函数,利用raw属性保留原始转义符,Windows路径/正则/代码生成场景
标签模板
模板前加标签函数,strings静态文本+values插值值自动分离,自定义处理逻辑
标签模板应用
XSS过滤(只转义插值)、金额格式化、多语言i18n、CSS-in-JS(styled-components)、SQL参数化
TS模板字面量类型
类型层面${}拼接,联合类型笛卡尔积,Uppercase/Lowercase/Capitalize,约束API路径/事件名/类名
插值任意表达式
变量/运算/三元/对象属性/数组方法/函数调用/嵌套/可选链,同步立即执行
异步插值
直接放异步函数显示[object Promise],需先await再插值或存ref/state
常见坑
反引号转义、插值副作用、非动态模板(封装函数)、异步Promise、空白保留(trim)、对象[object Object]

答对了几道?评论区报个数!觉得有用的话,点个关注支持一下~
JS/TS:模板字符串,告别繁琐字符串拼接

相关学习资料