文章出處

在 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">

因為這個改動是不向后兼容的,所以一些開發者們發現自己以前運行的好好的代碼突然報錯了:

轉義了 @ 和 % http://stackoverflow.com/questions/36953775/firefox-error-unable-to-check-input-because-the-pattern-is-not-a-valid-regexp

轉義了 ! 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 // 應該這么寫,可讀性比上面的更好

文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()