精讀《正則 ES2018》

黃子毅發表於2019-02-18

1. 引言

本週精讀的文章是 regexp-features-regular-expressions

這篇文章介紹了 ES2018 正則支援的幾個重要特性:

  • Lookbehind assertions - 後行斷言
  • Named capture groups - 命名捕獲組
  • s (dotAll) Flag - . 匹配任意字元
  • Unicode property escapes - Unicode 屬性轉義

2. 概述

還在用下標匹配內容嗎?匹配任意字元只有 [\w\W] 嗎?現在正則有更簡化的寫法了,事實上正則正在變得更加易用,是時候更新對正則的認知了。

2.1. Lookbehind assertions

完整的斷言定義分為:正/負向斷言 與 先/後行斷言 的笛卡爾積組合,在 ES2018 之前僅支援先行斷言,現在終於支援了後行斷言。

解釋一下這四種斷言:

正向先行斷言 (?=...) 表示之後的字串能匹配 pattern。

const re = /Item(?= 10)/;

console.log(re.exec("Item"));
// → null

console.log(re.exec("Item5"));
// → null

console.log(re.exec("Item 5"));
// → null

console.log(re.exec("Item 10"));
// → ["Item", index: 0, input: "Item 10", groups: undefined]
複製程式碼

負向先行斷言 (?!...) 表示之後的字串不能匹配 pattern。

const re = /Red(?!head)/;

console.log(re.exec("Redhead"));
// → null

console.log(re.exec("Redberry"));
// → ["Red", index: 0, input: "Redberry", groups: undefined]

console.log(re.exec("Redjay"));
// → ["Red", index: 0, input: "Redjay", groups: undefined]

console.log(re.exec("Red"));
// → ["Red", index: 0, input: "Red", groups: undefined]
複製程式碼

在 ES2018 後,又支援了兩種新的斷言方式:

正向後行斷言 (?<=...) 表示之前的字串能匹配 pattern。

先行時字串放前面,pattern 放後面;後行時字串放後端,pattern 放前面。先行匹配以什麼結尾,後行匹配以什麼開頭。

const re = /(?<=€)\d+(\.\d*)?/;

console.log(re.exec("199"));
// → null

console.log(re.exec("$199"));
// → null

console.log(re.exec("€199"));
// → ["199", undefined, index: 1, input: "€199", groups: undefined]
複製程式碼

負向後行斷言 (?<!...) 表示之前的字串不能匹配 pattern。

注:下面的例子表示 meters 之前 不能匹配 三個數字。

const re = /(?<!\d{3}) meters/;

console.log(re.exec("10 meters"));
// → [" meters", index: 2, input: "10 meters", groups: undefined]

console.log(re.exec("100 meters"));
// → null
複製程式碼

文中給了一個稍複雜的例子,結合了 正向後行斷言 與 負向後行斷言:

注:下面的例子表示 meters 之前 能匹配 兩個數字, 之前 不能匹配 數字 35.

const re = /(?<=\d{2})(?<!35) meters/;

console.log(re.exec("35 meters"));
// → null

console.log(re.exec("meters"));
// → null

console.log(re.exec("4 meters"));
// → null

console.log(re.exec("14 meters"));
// → ["meters", index: 2, input: "14 meters", groups: undefined]
複製程式碼

2.2. Named Capture Groups

命名捕獲組可以給正則捕獲的內容命名,比起下標來說更可讀。

其語法是 ?<name>

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const [match, year, month, day] = re.exec("2020-03-04");

console.log(match); // → 2020-03-04
console.log(year); // → 2020
console.log(month); // → 03
console.log(day); // → 04
複製程式碼

也可以在正規表示式中,通過下標 \1 直接使用之前的捕獲組,比如:

解釋一下,\1 代表 (\w\w) 匹配的內容而非 (\w\w) 本身,所以當 (\w\w) 匹配了 'ab' 後,\1 表示的就是對 'ab' 的匹配了。

console.log(/(\w\w)\1/.test("abab")); // → true

// if the last two letters are not the same
// as the first two, the match will fail
console.log(/(\w\w)\1/.test("abcd")); // → false
複製程式碼

對於命名捕獲組,可以通過 \k<name> 的語法訪問,而不需要通過 \1 這種下標:

下標和命名可以同時使用。

const re = /\b(?<dup>\w+)\s+\k<dup>\b/;

const match = re.exec("I'm not lazy, I'm on on energy saving mode");

console.log(match.index); // → 18
console.log(match[0]); // → on on
複製程式碼

2.3. s (dotAll) Flag

雖然正則中 . 可以匹配任何字元,但卻無法匹配換行符。因此聰明的開發者們用 [\w\W] 巧妙的解決了這個問題。

然而這終究是個設計缺陷,在 ES2018 支援了 /s 模式,這個模式下,. 等價於 [\w\W]

console.log(/./s.test("\n")); // → true
console.log(/./s.test("\r")); // → true
複製程式碼

2.4. Unicode Property Escapes

正則支援了更強大的 Unicode 匹配方式。在 /u 模式下,可以用 \p{Number} 匹配所有數字:

u 修飾符可以識別所有大於 0xFFFF 的 Unicode 字元。

const regex = /^\p{Number}+$/u;
regex.test("²³¹¼½¾"); // true
regex.test("㉛㉜㉝"); // true
regex.test("ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ"); // true
複製程式碼

\p{Alphabetic} 可以匹配所有 Alphabetic 元素,包括漢字、字母等:

const str = "漢";

console.log(/\p{Alphabetic}/u.test(str)); // → true

// the \w shorthand cannot match 漢
console.log(/\w/u.test(str)); // → false
複製程式碼

終於有簡便的方式匹配漢字了。

2.5. 相容表

可以到 原文 檢視相容表,總體上只有 Chrome 與 Safari 支援,Firefox 與 Edge 都不支援。所以大型專案使用要再等幾年。

3. 精讀

文中列舉的四個新特性是 ES2018 加入到正則中的。但正如相容表所示,這些特性基本還都不能用,所以不如我們再溫習一下 ES6 對正則的改進,找一找與 ES2018 正則變化的結合點。

3.1. RegExp 建構函式優化

當 RegExp 建構函式第一個引數是正規表示式時,允許指定第二個引數 - 修飾符(ES5 會報錯):

new RegExp(/book(?=s)/giu, "iu");
複製程式碼

不痛不癢的優化,,畢竟大部分時間建構函式不會這麼用。

3.2. 字串的正則方法

將字串的 match()replace()searchsplit 方法內部呼叫時都指向到 RegExp 的例項方法上,比如

String.prototype.match 指向 RegExp.prototype[Symbol.match]

也就是正規表示式原本應該由正則例項觸發,但現在卻支援字串直接呼叫(方便)。但執行時其實指向了正則例項物件,讓邏輯更為統一。

舉個例子:

"abc".match(/abc/g) /
  // 內部執行時,等價於
  abc /
  g[Symbol.match]("abc");
複製程式碼

3.3. u 修飾符

概述中,Unicode Property Escapes 就是對 u 修飾符的增強,而 u 修飾符是在 ES6 中新增的。

u 修飾符的含義為 “Unicode 模式”,用來正確處理大於 \uFFFF 的 Unicode 字元。

同時 u 修飾符還會改變以下正規表示式的行為:

  • 點字元原本支援單字元,但在 u 模式下,可以匹配大於 0xFFFF 的 Unicode 字元。
  • \u{61} 含義由匹配 61 個 u 改編為匹配 Unicode 編碼為 61 號的字母 a
  • 可以正確識別非單字元 Unicode 字元的量詞匹配。
  • \S 可以正確識別 Unicode 字元。
  • u 模式下,[a-z] 還能識別 Unicode 編碼不同,但是字型很近的字母,比如 \u212A 表示的另一個 K

基本上,在 u 修飾符模式下,所有 Unicode 字元都可以被正確解讀,而在 ES2018,又新增了一些 u 模式的匹配集合來匹配一些常見的字元,比如 \p{Number} 來匹配 ¼

3.4. y 修飾符

y 修飾符是 “粘連”(sticky)修飾符。

y 類似 g 修飾符,都是全域性匹配,也就是從上次成功匹配位置開始,繼續匹配。y 的區別是,必須是上一次匹配成功後的下一個位置就立即匹配才算成功。

比如:

/a+/g.exec("aaa_aa_a"); // ["aaa"]
複製程式碼

3.5. flags

通過 flags 屬性拿到修飾符:

const regex = /[a-z]*/gu;

regex.flags; // 'gu'
複製程式碼

4. 總結

本週精讀藉著 regexp-features-regular-expressions 這篇文章,一起理解了 ES2018 新增的正則新特性,又順藤摸瓜的整理了 ES6 對正則做的增強。

如果你擅長這種擴散式學習方式,不妨再進一步溫習一下整個 ES6 引入的新特性,筆者強烈推薦阮一峰老師的 ECMAScript 6 入門 一書。

ES2018 引入的特性還太新,單在對 ES6 特性的使用應該和對 ES3 一樣熟練。

如果你身邊的小夥伴還對 ES6 特性感到驚訝,請把這篇文章分享給他,防止退化為 “只剩專案經驗的 JS 入門者”。

討論地址是:精讀《正則 ES2018》 · Issue #127 · dt-fe/weekly

如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。