在 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 // 應該這么寫,可讀性比上面的更好
文章列表