前言
转义字符是JS的基础知识,按理说没有仔细研究的价值,但前不久我就遇到了一个关于字符串转义的问题,我想了很久才找到答案,于是我决定花时间仔细研究一下关于字符串转义的知识,总结成文章,希望对大家能有所帮助。
字符串字面量中反斜杠\的转义规则
在JS的字符串字面量中,反斜杠\
可以对任意的字符串进行转义,其转义规则如下——
特殊字符
如果字符是以下特殊字符之一,则使用反斜杠转义后,会被替换为对应的特殊字符:
\
本身 ->\
'
单引号 ->'
"
双引号 ->"
`
反引号 ->`
n
换行符 ->\n
(U+000A)r
回车符 ->\r
(U+000D)t
制表符 ->\t
(U+0009)b
退格符 ->\b
(U+0008)f
换页符 ->\f
(U+000C)v
垂直制表符 ->\v
(U+000B)八进制转义
如果反斜杠后面跟着的是1个0-7的八进制数字,则会被解释为八进制转义,结果就是八进制数字的字符串形式。例如:
\0
->\x00
\7
->\x07
\8
->8
超过7就不再转义十六进制转义
如果反斜杠后面跟着一个
x
,然后紧接2个十六进制数字(0-9、A-F、a-f),会被解释为Unicode转义,x
后接除此之外的字符会报错。例如:\x41
->A
(U+0041)\x7A
->z
(U+007A)\xhi
-> 报错Unicode 转义
如果反斜杠后面跟着一个
u
,然后紧接4个十六进制数字(0-9、A-F、a-f),会被解释为 Unicode 转义,u
后接除此之外的字符会报错。例如:\u0041
->A
(U+0041)\u007A
->z
(U+007A)\uhijk
-> 报错其它字符
如果反斜杠后面跟着其它字符,则会被解释为普通字符,加不加
\
没有区别。例如:\a
->a
\B
->B
正则表达式的双重转义问题
我们在使用正则表达式时,也会有字符串转义的问题,例如,如果要在正则中匹配字符串?
,由于它是正则中的特殊字符,用字面量的方式是这么写的:
var reg = /\?/
而如果我们使用字符串作为RegExp构造函数的参数,由于\
是JS字符中的特殊字符,但?
不是,因此需要这么写:
var reg = new RegExp("\\?")
而如果遇到字符串和正则都需要转义的情况时,例如反斜杠\
,使用RegExp构造函数就必须“双重转义”:
var reg = new RegExp("\\\\")
这种写法非常的不直观,让本就难以阅读的正则变得更难阅读,因此在实际开发中,我们还是尽量使用字面量来创建正则表达式。
但如果是动态的正则就没办法了,对此,MDN上提供了一个解决方案:
function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
这个escapeRegExp
函数可以将字符串中的特殊字符例如$
(
)
*
+
.
?
[
\
]
^
{
|
}
进行统一转义,利用它我们可以像正则字面量那样通过构造函数创建正则表达式,而无需额外考虑JS字符串本身的转义:
var reg = new RegExp(escapeRegExp("\\")) // 相当于 /\\/ 或 new RegExp("\\\\")
如何将纯文本的反斜杠变为转义符
这就是我文章开头提到的那个问题,这个需求发生在字符串替换的场景中,例如有这么一篇文章:
曾宴桃源深洞,一曲舞鸾歌凤。长记别伊时,和泪出门相送。如梦,如梦,残月落花烟重。
我需要在所有的句号。
后添加一个换行符\n
,假设我们是通过输入框的查找、替换来完成操作的:
var text = "曾宴桃源深洞,一曲舞鸾歌凤。长记别伊时,和泪出门相送。如梦,如梦,残月落花烟重。" // 伪代码 var $search = document.querySelector('#search') // 搜索输入框 var $replacement = document.querySelector('#$replacement') // 替换输入框 var result = text.replace(new RegExp($search.value), $replacement.value) // 替换
由于我们从输入框中拿到的字符串是纯文本的\n
,因此像上面这样直接替换的结果就是,\n
作为纯文本而不是换行符被添加到了句号后面:
曾宴桃源深洞,一曲舞鸾歌凤。\n长记别伊时,和泪出门相送。\n如梦,如梦,残月落花烟重。\n
显然这不是我们想要的,所以我们不得不先将纯文本的\n
替换为换行符:
var result = text.replace(new RegExp($search.value), $replacement.value.replaceAll('\\n', '\n'))
但这显然不是一个好的解决办法,JS中的特殊字符说多不多,但说少也不少,如果每一个都要手动替换,不仅麻烦,性能也堪忧:
$replacement.value .replaceAll('\\n', '\n') .replaceAll('\\r', '\r') .replaceAll('\\t', '\t') .replaceAll('\\\\', '\\') ...
我当时就一直困扰于这个问题,纯文本的反斜杠\
相当于字符串字面量的"\\"
,似乎除了一个个替换,没别的办法能一次性将所有的转义符还原。
但经过一番思考,我最终想到了一个绝妙的解决方案,那就是利用JSON.parse
:
function deEscape(str) { return JSON.parse(`"${str}"`) } var result = text.replace(new RegExp($search.value), deEscape($replacement.value))
deEscape
函数可以将纯文本的斜杠\
识别为转义符,这其中的原理还真有点“只可意会,不可言传”的味道,不知道看这篇文章的你有没有“品”出来。
如何将反斜杠作为纯文本避免转义
这个倒是比较简单,如果我们希望字符串字面量中的反斜杠\
不要作为转义字符,可以使用String.raw
,它是个标签模板方法,例如:
var str = String.raw`\n` // 这里的反斜杠\会被解释为普通字符而不是转义字符 console.log(str) // 相当于字面量"\\n"
但要注意的是,不能使用插值语句,例如:
var s1 = '\n' var str = String.raw`${s1}` console.log(str) // 相当于字面量"\n",依旧是换行符
因为这里的s1在被赋值之时就已经被解释成换行符了,将换行符插入到模板字符串中,它依旧是换行符。只有在标签模板方法中,在生成字符串的过程中它会被解释为普通字符。