研究一下JavaScript中字符串的转义规则
前言
转义字符是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
正则表达式的双重转义问题
我们在使用正则表达式时,也会有字符串转义的问题,例如,如果要在正则中匹配字符串?
,由于它是正则中的特殊字符,用字面量的方式是这么写的:
js
var reg = /\?/
而如果我们使用字符串作为RegExp构造函数的参数,由于\
是JS字符中的特殊字符,但?
不是,因此需要这么写:
js
var reg = new RegExp("\\?")
而如果遇到字符串和正则都需要转义的情况时,例如反斜杠\
,使用RegExp构造函数就必须"双重转义":
js
var reg = new RegExp("\\\\")
这种写法非常的不直观,让本就难以阅读的正则变得更难阅读,因此在实际开发中,我们还是尽量使用字面量来创建正则表达式。
但如果是动态的正则就没办法了,对此,MDN上提供了一个解决方案:
js
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
这个escapeRegExp
函数可以将字符串中的特殊字符例如$
(
)
*
+
.
?
[
\
]
^
{
|
}
进行统一转义,利用它我们可以像正则字面量那样通过构造函数创建正则表达式,而无需额外考虑JS字符串本身的转义:
js
var reg = new RegExp(escapeRegExp("\\")) // 相当于 /\\/ 或 new RegExp("\\\\")
如何将纯文本的反斜杠变为转义符
这就是我文章开头提到的那个问题,这个需求发生在字符串替换的场景中,例如有这么一篇文章:
bash
曾宴桃源深洞,一曲舞鸾歌凤。长记别伊时,和泪出门相送。如梦,如梦,残月落花烟重。
我需要在所有的句号。
后添加一个换行符\n
,假设我们是通过输入框的查找、替换来完成操作的:
js
var text = "曾宴桃源深洞,一曲舞鸾歌凤。长记别伊时,和泪出门相送。如梦,如梦,残月落花烟重。"
// 伪代码
var $search = document.querySelector('#search') // 搜索输入框
var $replacement = document.querySelector('#$replacement') // 替换输入框
var result = text.replace(new RegExp($search.value), $replacement.value) // 替换
由于我们从输入框中拿到的字符串是纯文本的\n
,因此像上面这样直接替换的结果就是,\n
作为纯文本而不是换行符被添加到了句号后面:
bash
曾宴桃源深洞,一曲舞鸾歌凤。\n长记别伊时,和泪出门相送。\n如梦,如梦,残月落花烟重。\n
显然这不是我们想要的,所以我们不得不先将纯文本的\n
替换为换行符:
js
var result = text.replace(new RegExp($search.value), $replacement.value.replaceAll('\\n', '\n'))
但这显然不是一个好的解决办法,JS中的特殊字符说多不多,但说少也不少,如果每一个都要手动替换,不仅麻烦,性能也堪忧:
js
$replacement.value
.replaceAll('\\n', '\n')
.replaceAll('\\r', '\r')
.replaceAll('\\t', '\t')
.replaceAll('\\\\', '\\')
...
我当时就一直困扰于这个问题,纯文本的反斜杠\
相当于字符串字面量的"\\"
,似乎除了一个个替换,没别的办法能一次性将所有的转义符还原。
但经过一番思考,我最终想到了一个绝妙的解决方案 ,那就是利用JSON.parse
:
js
function deEscape(str) {
return JSON.parse(`"${str}"`)
}
var result = text.replace(new RegExp($search.value), deEscape($replacement.value))
deEscape
函数可以将纯文本的斜杠\
识别为转义符,这其中的原理还真有点"只可意会,不可言传"的味道,不知道看这篇文章的你有没有"品"出来。
如何将反斜杠作为纯文本避免转义
这个倒是比较简单,如果我们希望字符串字面量中的反斜杠\
不要作为转义字符,可以使用String.raw
,它是个标签模板方法,例如:
js
var str = String.raw`\n` // 这里的反斜杠\会被解释为普通字符而不是转义字符
console.log(str) // 相当于字面量"\\n"
但要注意的是,不能使用插值语句,例如:
js
var s1 = '\n'
var str = String.raw`${s1}`
console.log(str) // 相当于字面量"\n",依旧是换行符
因为这里的s1在被赋值之时就已经被解释成换行符了,将换行符插入到模板字符串中,它依旧是换行符。只有在标签模板方法中,在生成字符串的过程中它会被解释为普通字符。