Skip to content

字符串的扩展

ES6 对字符串做了大量增强,其中最重要的就是模板字符串。

模板字符串#

基本语法#

用反引号 ` 包裹:

const name = '张三'
const greeting = `你好,${name}`
console.log(greeting) // 你好,张三!

多行字符串#

// ES5 多行字符串
var html = '<div>\n' + ' <h1>标题</h1>\n' + '</div>'
// ES6
const html = `<div>
<h1>标题</h1>
</div>`

所有空格和换行都会保留:

const text = `第一行
第二行(前面有两个空格)
第三行`
console.log(text)
// 第一行
// 第二行(前面有两个空格)
// 第三行

表达式嵌入#

${} 中可以是任意 JavaScript 表达式:

const a = 10
const b = 20
console.log(`${a} + ${b} = ${a + b}`) // 10 + 20 = 30
// 调用函数
function getName() {
return '张三'
}
console.log(`用户:${getName()}`) // 用户:张三
// 三元表达式
const score = 85
console.log(`成绩:${score >= 60 ? '及格' : '不及格'}`) // 成绩:及格
// 访问对象属性
const user = { name: '张三', age: 25 }
console.log(`${user.name}${user.age}`) // 张三,25岁

嵌套模板#

const items = ['苹果', '香蕉', '橙子']
const list = `
<ul>
${items.map((item) => `<li>${item}</li>`).join('\n ')}
</ul>
`
console.log(list)
// <ul>
// <li>苹果</li>
// <li>香蕉</li>
// <li>橙子</li>
// </ul>

转义#

在模板字符串中使用反引号或 ${

const code = `使用 \`反引号\`\${表达式}`
console.log(code) // 使用 `反引号` 和 ${表达式}

标签模板#

标签模板(Tagged Template)是模板字符串的高级用法,函数名后面直接跟模板字符串:

function tag(strings, ...values) {
console.log(strings) // ['Hello ', '!']
console.log(values) // ['World']
}
tag`Hello ${'World'}!`

参数说明#

const name = '张三'
const age = 25
function myTag(strings, ...values) {
console.log(strings) // ['用户', ',年龄', '']
console.log(values) // ['张三', 25]
console.log(strings.raw) // 原始字符串数组
}
myTag`用户${name},年龄${age}`

实用场景:HTML 转义#

function safeHTML(strings, ...values) {
const escape = (str) =>
String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
return strings.reduce((result, str, i) => {
const value = values[i - 1]
return result + escape(value) + str
})
}
const userInput = '<script>alert("xss")</script>'
const html = safeHTML`<div>${userInput}</div>`
console.log(html)
// <div>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</div>

实用场景:国际化#

const i18n = {
'zh-CN': {
greeting: (name) => `你好,${name}`,
},
'en-US': {
greeting: (name) => `Hello, ${name}!`,
},
}
function t(strings, ...values) {
const locale = 'zh-CN' // 假设从某处获取
const key = strings.join('$')
const template = i18n[locale][key]
return template ? template(...values) : strings.join('')
}
// 需要更复杂的实现,这里只是示例思路

实用场景:样式组件#

CSS-in-JS 库如 styled-components 就是用标签模板实现的:

// 简化版实现
function styled(strings, ...values) {
const css = strings.reduce((result, str, i) => {
return result + str + (values[i] || '')
}, '')
return function (props) {
return {
style: css,
props,
}
}
}
const Button = styled`
background: blue;
color: white;
padding: 10px 20px;
`

实用场景:SQL 防注入#

function sql(strings, ...values) {
const escaped = values.map((v) => {
if (typeof v === 'string') {
return `'${v.replace(/'/g, "''")}'`
}
return v
})
return strings.reduce((query, str, i) => {
return query + str + (escaped[i - 1] ?? '')
})
}
const name = "O'Brien"
const query = sql`SELECT * FROM users WHERE name = ${name}`
console.log(query)
// SELECT * FROM users WHERE name = 'O''Brien'

Unicode 支持#

码点表示法#

ES6 支持 \u{码点} 语法:

// ES5 只能表示 \u0000 到 \uFFFF
console.log('\u0041') // A
console.log('\u20BB7') // 乱码,被解析为 \u20BB + 7
// ES6 大括号语法
console.log('\u{20BB7}') // 𠮷
console.log('\u{1F600}') // 😀

字符串遍历器#

for...of 正确识别 32 位字符:

const text = '𠮷a'
// for 循环无法正确处理
for (let i = 0; i < text.length; i++) {
console.log(text[i]) // 乱码, 乱码, a
}
// for...of 正确处理
for (const char of text) {
console.log(char) // 𠮷, a
}
// 获取正确长度
console.log([...text].length) // 2
console.log(text.length) // 3

codePointAt 和 fromCodePoint#

const s = '𠮷a'
// charCodeAt 无法正确处理
s.charCodeAt(0).toString(16) // d842
// codePointAt 正确返回码点
s.codePointAt(0).toString(16) // 20bb7
// 从码点创建字符
String.fromCodePoint(0x20bb7) // 𠮷
String.fromCodePoint(0x1f600) // 😀
// fromCharCode 无法处理大于 0xFFFF 的码点
String.fromCharCode(0x20bb7) // 乱码

normalize#

处理 Unicode 组合字符:

// 同一个字符可能有不同的表示方式
const s1 = '\u01D1' // Ǐ(一个字符)
const s2 = '\u004F\u030C' // O + ̌(两个字符组合)
console.log(s1 === s2) // false
console.log(s1.length, s2.length) // 1, 2
// normalize 统一表示
console.log(s1.normalize() === s2.normalize()) // true

String.raw#

获取模板字符串的原始内容:

console.log(`line1\nline2`)
// line1
// line2
console.log(String.raw`line1\nline2`)
// line1\nline2
// 实际应用:Windows 路径
const path = String.raw`C:\Users\name\file.txt`
console.log(path) // C:\Users\name\file.txt

也可以作为函数调用:

String.raw({ raw: ['a', 'b', 'c'] }, 1, 2)
// 'a1b2c'

实战应用#

HTML 模板#

function render(data) {
return `
<!DOCTYPE html>
<html>
<head>
<title>${data.title}</title>
</head>
<body>
<h1>${data.title}</h1>
<ul>
${data.items.map((item) => `<li>${item}</li>`).join('')}
</ul>
</body>
</html>
`
}
const html = render({
title: '商品列表',
items: ['苹果', '香蕉', '橙子'],
})

日志格式化#

function log(level) {
return function (strings, ...values) {
const message = strings.reduce((acc, str, i) => {
return acc + str + (values[i] ?? '')
}, '')
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] [${level}] ${message}`)
}
}
const info = log('INFO')
const error = log('ERROR')
const user = '张三'
info`用户 ${user} 登录成功`
// [2024-01-01T00:00:00.000Z] [INFO] 用户 张三 登录成功

简单的模板引擎#

function template(str) {
return function (data) {
return str.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return data[key] ?? ''
})
}
}
const tpl = template('你好,{{name}}!你的余额是 {{balance}} 元。')
console.log(tpl({ name: '张三', balance: 100 }))
// 你好,张三!你的余额是 100 元。

小结#

特性语法用途
模板字符串`${expr}`字符串插值、多行文本
标签模板fn`...`自定义字符串处理
Unicode 支持\u{码点}完整 Unicode 表示
String.rawString.raw`...`获取原始字符串

模板字符串让字符串操作变得更加直观,标签模板则为 DSL(领域特定语言)提供了强大的基础设施。