写正则不要再瞎转义了
在 JavaScript 中,有两个地方用到了反斜杠转义序列,一个是在字符串字面量里,一个是在正则字面量里。其中字符串字面量里的反斜杠转义序列又分为下面几种形式:
1. \ 后面跟着单引号(')、双引号(")、反斜杠自己(\)、b、f、n、r、t、v 其中的一个
2. \ 后面跟着某个行终止符序列,常见的行终止符序列有三种:回车、换行、回车+换行
3. \ 后面跟着 0
4. \ 后面跟着 1 到 3 个八进制数字
5. \ 后面跟着 x 再跟着两个 16 进制数字
6. \ 后面跟着 u 再跟着 4 个 16 进制数字
7. \ 后面跟着 u 再跟着 {1个到任意多个的 16 进制数字}
8. \ 后面跟着某个不满足上面所有这些条件的单个字符
前 7 种本文不予讨论,这第 8 种转义形式其实就是无效的转义。举个例子,比如 \o 就是这样的转义,在一些编译语言里,这样的转义会直接报编译错误,比如在 Java 里:
System.out.println("\o"); // error: illegal escape character
另外在 JSON 里也会报错:
JSON.parse(String.raw`"\o"`) // SyntaxError: Unexpected token o in JSON at position 2
在脚本语言里通常都不会报错,它们有两种选择,要么不把 \ 看成转义字符,而是看成普通的反斜杠字面量,像 Python:
>>> "\o" '\\o' # len("\o") 为 2
要么把 \ 丢弃掉,只留下后面被转义的那个字符,像 JavaScript:
js> "\o" "o" // "\o".length 为 1
哪种做法好呢?我知道一个保留反斜杠的好处是和正则相关的:在一些没有正则字面量的语言里,或者是有些人不会用正则字面量,又或者有时需要从字符串变量动态生成正则的时候,有些人会忘了要双写反斜杠,比如:
"www\.taobao\.com" // 在不保留反斜杠的语言比如 JavaScript 里,这个字符串生成的正则可以错误的匹配到别的域名,比如 "wwwataobao.com"
还有些语言会对这个坑会发出警告:
$ awk -F 'www\.taobao\.com' '' awk: warning: escape sequence `\.' treated as plain `.'
丢弃反斜杠在其它语言里有什么好处我不清楚,但在 JavaScript 里,我还真知道一个,那就是在一个内联的 <script> 标签里书写一个包含有 </script> 字样的字符串时:
<script> document.wirte("<script src=foo.js><\/script>") // \/ 其实是无效的转义,但没有 \ 的话,这个 </script> 会被 HTML 解析器错误的当成结束标签 </script>
下面才开始本文的正文,正则字面量中的反斜杠转义序列比字符串字面量中的更复杂些,分为下面这么多种形式:
1. \ 后面跟着 /
2. \ 后面跟着 ^、$、\、.、*、+、?、(、)、[、]、{、}、| 其中的一个
3. \ 后面跟着 c 再跟任意一个字母
4. \ 不存在于 [] 中,后面跟着 b 或者 B
5. \ 存在于于 [] 中,后面跟着 - 或者 b
6. \ 后面跟着 d、D、s、S、w、W 其中的一个
7. \ 后面跟着 f、n、r、t、v 其中的一个
8. \ 后面跟着 0
9. \ 不存在于 [] 中,后面跟着 1 到 3 个十进制数字
10. \ 后面跟着 x 再跟着两个 16 进制数字
11. \ 后面跟着 u 再跟着 4 个 16 进制数字
12. \ 后面跟着 u 再跟着 {1个到任意多个的 16 进制数字}
13. \ 后面跟着某个不满足上面所有这些条件的单个字符
上面这些转义形式有些和字符串字面量中的相同,有些则不同,甚至有些虽然外表看起来相同,功能却不同。我们还是重点关注最后一种情况,也就是无效转义的情况。很多人记不住在正则里哪些符号应该转义,哪些不该转义,比如双引号 " 在正则里是不需要转义的,如果你写了 /\"/,通常情况下,JavaScript 的正则引擎会帮你把 \ 去掉:
/^\"$/.test('"') // true
但如果这个正则开启了 Unicode 模式,则这样的写法会导致语法错误:
/\"/u // SyntaxError: Invalid escape
那你可能会问,为什么要开启 Unicode 模式呢?这是因为 Unicode 模式对 BMP 之外的字符支持更友好,比如:
"𠮷野家".match(/./g) // ["�", "�", "野", "家"] "𠮷野家".match(/./ug) // ["𠮷", "野", "家"]
具体的优点可以看这篇文章的总结,总之,如果不考虑兼容性的话,默认加上 /u 总是最佳做法。
还有一个经常被错误转义的字符,那就是连字符 -,连字符只在中括号里面才是元字符,在中括号里面需要转义,但如果你在中括号外面转义它的话,同样在 Unicode 模式下会报错:
/\-/u // SyntaxError: Invalid escape
另外,从 Firefox 46 和 Chrome 53 开始,在 HTML 表单的 pattern 属性中填写的正则开始强制使用 Unicode 模式,比如下面这个 input 的 pattern 属性就是无效的。打开下面这个 demo,然后打开开发者工具,然后把鼠标指针移动到 input 上,就能看到开发者工具的控制台出现了报错信息:
<input pattern="\-" value="foo">
因为这个改动是不向后兼容的,所以一些开发者们发现自己以前运行的好好的代码突然报错了:
转义了 ! https://input.mozilla.org/en-US/dashboard/response/5898357
转义了 - http://stackoverflow.com/questions/39895209/html-input-pattern-not-working
转义了 ' https://bugs.chromium.org/p/chromium/issues/detail?id=667713
可以看见,只要是标点符号,就有人想转义,因为他们对正则不熟悉,不知道哪些符号是元字符,以前这样做没事,但从现在开始,不行了。
Unicode 模式就像是正则里的严格模式,禁止了很多不好的、容易导致 bug 的写法,下面再举一个 /u 禁止了的、和 \ 转义相关的写法,那就是 \ 后面跟非 0 十进制数字的情况:
在非 Unicode 模式下,当 \ 后面是一个非 0 的十进制数字时,如果这个数字对应的捕获分组刚好存在,则该转义序列表示反向引用那个分组:
/(f)(.)\2/.test("foo") // true
如果对应的捕获分组不存在,且数字 < 8 的话,则该序列会被当做八进制转义序列看待:
/^\2$/.test("\2") // true
如果对应的捕获分组不存在,且数字 >= 8 的话,反斜杠会被丢弃,只留下数字:
/^\8$/.test("8") // true
也就是说,一样的转义写法可能有三种不同的解释,稍不留神就会导致 bug,代码也不好读,因此 Unicode 模式禁用了后两种情况,\ 后跟非 0 十进制数字只能表示捕获分组的反向引用,只要对应的捕获分组不存在,就报语法错误:
/\2/u // SyntaxError: Invalid escape /\8/u // SyntaxError: Invalid escape
总结:本文列举了几个在正则的 Unicode 模式下不正确的转义形式,告诫大家以后在写正则的时候不能看到标点符号就想转义,对待知识要一丝不苟。
更高要求:其实正则的 Unicode 模式并没有我希望的那么严格,比如正则里的大多数元字符,实际上在中括号里并不是元字符,是不需要转义的,但即便 Unicode 模式下也并不会禁止这样的写法:
/[\?\+\*]/u // 不会报错 /[?+*]/u // 应该这么写,可读性比上面的更好