小覽 ES6-ES2019 中正則表達式的新發展

在此前的 《JS正則表達式--從入門到精分》 一文中,曾經較完整的介紹過 Javascript 中正則表達式的用法。而從 ES6(ES2015) 開始,藉助 Babel 等標誌性的工具,JS 的發展彷佛也不想重蹈 Flash 時代的無所做爲,走上了每一年一個小版本的快車道;在此過程當中,正則表達式也陸續演化出一些新的特性。javascript

本文嘗試蜻蜓點水,一瞥這些特性,看可否籍此簡化咱們的開發。html

ECMAScript 和 TC39

雖然多是你們廣泛瞭解的事情,但這些稱呼反覆出現,可能仍是須要稍微解釋一下先。java

ECMA 指的是 「歐洲計算機制造商協會」(European Computer Manufacturers Association);如今也稱爲 「ECMA 國際」(ECMA International),定位爲一家國際性會員制度的信息和電信標準組織。git

1996 年 11 月,JavaScript 的創造者 Netscape 公司,決定將 JavaScript 提交給 ECMA,但願這種語言可以成爲國際標準。次年,ECMA 發佈 262 號標準文件(ECMA-262)的初版,規定了瀏覽器腳本語言的標準,並將這種語言稱爲 ECMAScript (簡稱 ES)。es6

此後該標準應用普遍,JavaScript、JScript、ActionScript 等都算是 ECMA-262 標準的實現和擴展。github

從 1997 年至 2009 年,陸續發佈了 ES1 ~ ES5;其中 ES4 實際上是偏向於 Adobe 的 ActionScript 風格的實踐設計的,但最終也隨着 Flash 被市場上其餘廠商封殺,以改動過大爲名不了了之,但其中一些特性被後來的 ES6 繼承。正則表達式

2015年,能夠說迄今最重要的一個版本 ES6,也就是 ES2015(ES6 的第一個版本) 發佈。express

由各個主流瀏覽器廠商的表明組成 ECMA 第 39 號技術專家委員會(Technical Committee 39,簡稱 TC39),負責制訂新的 ECMAScript 標準。瀏覽器

新的語法從提案到變成正式標準,須要經歷五個階段。每一個階段的變更都須要由 TC39 委員會批准:babel

  • Stage 0 - Strawman(展現階段)
  • Stage 1 - Proposal(徵求意見階段)
  • Stage 2 - Draft(草案階段)
  • Stage 3 - Candidate(候選人階段)
  • Stage 4 - Finished(定案階段)

以上這幾個也就是咱們以前使用 Babel 轉譯工具時會引入 babel-preset-stage-0 等預置方案的起因,固然隨着 Babel 7 的發佈,這些方案都被統一到了 @babel/preset-env 中。

ES6 中的正則表達式特性

如下特性首次在 ES6 中出現:

  • 「粘性」修飾符 /y
  • unicode 修飾符 /u
  • 正則表達式對象上的新屬性 flags
  • 用構造函數 RegExp() 拷貝正則表達式

「粘性」修飾符 /y

修飾符 /y 只將正則表達式的每一個匹配錨定到前一個匹配的末尾

簡單的說,這主要與正則表達式對象上的 lastIndex 屬性有關 -- 其與 /g/y 的搭配,會產生不一樣的效果。

在不設置修飾符,或只設置了 /g 修飾符的狀況下,只要目標字符串(或上一次匹配的剩餘部分)中存在匹配就能夠。

/y 修飾符則告知正則表達式,只能不偏不倚的從字符串的 lastIndex 那個位置去匹配,這也就是「粘性、粘連」的涵義。

exec() 的使用爲例:

//不設置修飾符
const re1 = /a/;
re1.lastIndex = 7; // 設置的十分明確然而並不會有什麼用
const match = re1.exec('haha');
console.log(match.index); // 1
console.log(re1.lastIndex); // 7 (沒有變化呢)

//設置了 `/g` 修飾符
const re2 = /a/g;
re2.lastIndex = 2;
const match = re2.exec('haha');
console.log(match.index); // 3 (此次被 lastIndex 影響了)
console.log(re2.lastIndex); // 4 (更新爲匹配成功後的下一位了)
console.log(re2.exec('xaxa')); // null (4之後就沒有再匹配的了)

//設置了 `/y` 修飾符
const re3 = /a/y;
re3.lastIndex = 2;
console.log(re3.exec('haha')); // null (在位置 2 並不匹配)
re3.lastIndex = 3;
const match = re3.exec('haha');
console.log(match.index); // 3 (在位置 3 準確匹配了)
console.log(re3.lastIndex); // 4 (也更新了)
複製代碼

固然,通常狀況下 -- 好比第一次運行匹配,或不特別設置 lastIndex 時,/y 的功效大抵和 ^ 起始匹配符相同,由於此時 lastIndex0 :

const re1 = /^a/g;
const re2 = /a/y;
console.log(re1.test('haha')); // false
console.log(re2.test('haha')); // false
複製代碼

須要注意的是,若是同時設置了 /g/y,則只有 /y 會生效。

sticky 屬性

/y 修飾符相配套,ES6 的正則表達式對象多了 sticky 屬性,表示是否設置了 /y 修飾符:

var r = /hello\d/y;
r.sticky // true
複製代碼

unicode 修飾符 /u

這裏簡單解釋一下 Unicode,其目標是爲世界上每個字符提供惟一標識符,該惟一標識符可稱爲 碼點(code point) 或 字符編碼(character encode)。

在 ES6 以前, JS 的字符串以 16 位字符編碼(UTF-16)爲基礎。每一個 16 位序列(至關於2個字節)是一個編碼單元(code unit,可簡稱爲碼元),用於表示一個字符。字符串全部的屬性與方法(如length屬性與charAt() 方法等)都是基於這樣 16 位的序列。

原本 JS 容許採用 \uxxxx 形式表示一個經常使用的 unicode 字符,其中的 4 個十六進制數字表示字符的 unicode 碼點:

console.log("\u0061"); // "a"
複製代碼

同時,這種表示法只能表示碼點侷限於 0x0000~0xFFFF 之間的字符。超出這個範圍的字符,必須用兩個碼元鏈接的形式(稱爲 surrogate-pairs,代理對)表示一個碼點:

console.log("\uD842\uDFB7"); // "𠮷"
複製代碼

這就致使了一個問題,對於一些超出 16 位 0xFFFF 的 unicode 字符,傳統的方法就會出錯;好比直接在 \u20BB7,JS 會理解成 \u20BB + 7;因此會顯示成一個特殊字符,後面跟着一個 7。

對此 ES6 作出了改進,將 unicode 編碼放入大括號(這種語法稱爲 unicode 碼點轉義符),就能夠正確解讀字符了:

console.log("\u{20BB7}"); // "𠮷"
複製代碼

一樣的例子:

'\u{1F680}' === '\uD83D\uDE80' //true

console.log('\u{1F680}') //🚀
console.log('\uD83D\uDE80') //🚀
複製代碼

此中的轉換對應關係,這篇文章( blog.csdn.net/hherima/art… )作了比較清楚的探尋,感興趣的話能夠結合文末的資料進行研究;本文不展開掰哧,能體會示例便可。

書歸正傳,在 ES6 的正則中:

修飾符 /u 將正則表達式切換爲特殊的 Unicode 模式

在 Unicode 模式下,既可使用新的大括號 unicode 編碼點轉義符表示範圍更大的字符,也能夠繼續使用 UTF-16 碼元。該模式具備以下特徵:

  • 「單獨代理」(lone surrogates)特性:
//傳統的非 Unicode 模式
/\uD83D/.test('\uD83D\uDC2A') //true,按16位碼元識別

//Unicode 模式
/\uD83D/u.test('\uD83D\uDC2A') //false,此模式下會識別成碼點中的原子部分
/\uD83D/u.test('\uD83D \uD83D\uDC2A') //true
/\uD83D/u.test('\uD83D\uDC2A \uD83D') //true
複製代碼
  • 能夠將碼點放入正則的字符類中:
/^[\uD83D\uDC2A]$/.test('\uD83D\uDC2A') //false
/^[\uD83D\uDC2A]$/u.test('\uD83D\uDC2A') //true

/^[\uD83D\uDC2A]$/.test('\uD83D') //true
/^[\uD83D\uDC2A]$/u.test('\uD83D') //false
複製代碼
  • 點操做符匹配碼點,而非碼元
'\uD83D\uDE80'.match(/./gu).length //1
'\uD83D\uDE80'.match(/./g).length //2
複製代碼
  • 數量描述符也一樣匹配到碼點
/\uD83D\uDE80{2}/u.test('\uD83D\uDE80\uD83D\uDE80') //true
/\uD83D\uDE80{2}/.test('\uD83D\uDE80\uD83D\uDE80') //false
/\uD83D\uDE80{2}/.test('\uD83D\uDE80\uDE80') //true
複製代碼

正則表達式對象上的新屬性 flags

新增的 flags 屬性,會返回正則表達式的修飾符

const re = /abc/ig;
console.log( re.source ); //'abc'
console.log( re.flags ); //'gi'
複製代碼

RegExp() 拷貝正則表達式

正則表達式構造函數的傳統簽名是 new RegExp(pattern : string, flags = ''),好比:

const re1 = new RegExp("^a\d{3}", 'gi')
// 等同於: /^ad{3}/gi
複製代碼

ES6中,新增的用法是 new RegExp(regex : RegExp, flags = regex.flags)

var re2 = new RegExp(re1, "yi")
// 結果是: /^ad{3}/iy
複製代碼

這就提供了一種拷貝已有正則表達式,或更改其修飾符的方法。

ES2018/ES2019 中的新特性

在 ES2018 - ES2019 中,又增長了一些特性:

  • 命名捕獲組
  • 反向引用
  • 反向斷言
  • unicode 屬性轉義
  • dotAll 修飾符 /s

命名捕獲組

此前的正則表達式操做中,採用的是「編號捕獲組」(Numbered capture groups)匹配字符串並將之分組,好比:

const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/; //小括號的順序決定了其編號
const matchObj = RE_DATE.exec('1999-12-31');

const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31
複製代碼

這種方式不管從易用性仍是複雜度方面都不太理想,尤爲當字段過多、存在嵌套等狀況時;若是改動了正則表達式還容易忘記同步改變分散在各處的編號。

ES6 帶來的「命名捕獲組」(Named capture groups),則能夠經過名稱來識別捕獲的分組

  • 其格式如 (?<year>[0-9]{4})
  • 經過捕獲結果中的 groups.year 屬性取出
  • 任何匹配失敗的命名組都將返回 undefined
const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

//編號的方式同時被保留
const year2 = matchObj[1]; // 1999
const month2 = matchObj[2]; // 12
const day2 = matchObj[3]; // 31
複製代碼

這還爲 replace() 方法提供了額外的便利,注意其語法:

const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
console.log( '2018-04-30'.replace(RE_DATE, '$<month>-$<day>-$<year>') ); //04-30-2018

//編號捕獲組的寫法:
console.log( '2018-04-30'.replace(RE_DATE, "$2-$3-$1") ); //04-30-2018
複製代碼

反向引用(Backreferences)

正則表達式中的 \k<name> 表示這樣的意思:根據前一次匹配到的命名捕獲組中的名稱,匹配相應的字符串,好比:

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc'); // true
RE_TWICE.test('abc!ab'); // false
複製代碼

這種稱爲反向引用的語法,對於編號捕獲組一樣適用:

const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
RE_TWICE.test('abc!abc'); // true
RE_TWICE.test('abc!ab'); // false
複製代碼

兩摻兒的,也沒問題:

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
RE_TWICE.test('abc!abc!abc'); // true
RE_TWICE.test('abc!abc!ab'); // false
複製代碼

反向斷言(lookbehind assertions)

根據以前文章的介紹,JS 中已經支持了「正向斷言」(Lookahead assertions),或稱爲正向查找。

  • x(?=y) 匹配'x'僅僅當'x'後面跟着'y'。稱爲正向確定查找
  • x(?!y) 匹配'x'僅僅當'x'後面不跟着'y'。稱爲正向否認查找

ES2018 引入了反向斷言(lookbehind assertions),與正向斷言的工做方式相同,只是方向相反

一樣也分爲兩種子類型:

  • y(?<=x) 匹配'x'僅僅當'x'前面挨着'y'。稱爲反向確定查找
//傳統的作法:
const RE_DOLLAR_PREFIX = /(\$)foo/g;
'$foo %foo foo'.replace(RE_DOLLAR_PREFIX, '$1bar'); // '$bar %foo foo'
    
//用了反向確定查找的方法:
const RE_DOLLAR_PREFIX = /(?<=\$)foo/g;
'$foo %foo foo'.replace(RE_DOLLAR_PREFIX, 'bar'); // '$bar %foo foo'
複製代碼
  • y(?<!x) 匹配'x'僅僅當'x'前面不挨着'y'。稱爲反向否認查找
//傳統的作法:
const RE_NO_DOLLAR_PREFIX = /([^\$])foo/g
'$foo %foo *foo'.replace(RE_NO_DOLLAR_PREFIX, '$1bar');  //"$foo %bar *bar"

//用了反向確定查找的方法:
const RE_NO_DOLLAR_PREFIX = /(?<!\$)foo/g;
'$foo %foo foo'.replace(RE_NO_DOLLAR_PREFIX, 'bar'); // '$foo %bar bar'
複製代碼

unicode 屬性轉義

在 ES6 的 /u 修飾符基礎上,ES2018 添加了 "unicode 屬性轉義"(Unicode property escapes) -- 形式爲 \p{...}\P{...},分別表示「包含」和「不包含」

從目的和形式上這很相似於用 \s 來匹配空格等 whitespace,而 \p{}\P{} 花括號中的部分稱爲 "unicode 字符屬性"(Unicode character properties),讓正則表達式有了更好的可讀性。

/^\p{Script=Greek}+$/u.test('μετά') //true,匹配希臘字母,prop=value 的形式

/^\p{White_Space}+$/u.test('\t \n\r') //true,匹配全部空格,bin_prop 的形式
複製代碼

所謂「unicode 字符屬性」,是指在 Unicode 標準中,每一個字符都有用於描述其性質的元數據: properties,好比:

  • Name: 一個惟一的名稱,由大寫字母、數字、連字符、空格組成,如:
    • A: Name = LATIN CAPITAL LETTER A
    • 😀: Name = GRINNING FACE
  • General_Category: 分類的字符,如:
    • x: General_Category = Lowercase_Letter
    • $: General_Category = Currency_Symbol
  • White_Space: 用於標記不可見的空格、製表符、換行等字符,如:
    • \t: White_Space = True
    • π: White_Space = False
  • Age: Unicode 標準的版本號,如:
    • : Age = 2.1
  • Block: 碼點的一個連續範圍,不會重複,命名也是惟一的,如:
    • S: Block = Basic_Latin (range U+0000..U+007F)
    • 😀: Block = Emoticons (range U+1F600..U+1F64F)
  • Script: 一個字符集合,用於一個或多個書寫系統
    • 某些 script 支持多個書寫系統,好比 Latin script 支持 English, French, German, Latin 等
    • 某些語言能夠用由多種 script 支持的多種替代書寫系統書寫。例如,土耳其語在 20 世紀早期轉變爲 Latin script 以前就使用了 Arabic script。
    • 舉例來講:
      • α: Script = Greek
      • Д: Script = Cyrillic

另外幾個例子:

"AaBbCcDD".split("").filter(letter=>{
    return /\p{Lower}/u.test(letter);
}).join("") //"abc"

const regex = /^\p{Number}+$/u;
regex.test('²³¾𝟏𝟞𝟩㉝ⅠⅡⅻⅼⅽⅾⅿ'); //true

/\p{Currency_Symbol}+/u.test("¥$€"); //true
複製代碼

dotAll 修飾符 /s

咱們一般會在不少正則表達式中見到一種 [\s\S] 的匹配小技巧,這種看似多餘的寫法實際上是爲了彌補 . 標記沒法在多行的狀況下實現正確匹配的缺憾。

修飾符 /s 解決了這個問題,因此也稱爲 dotAll 修飾符。

console.log(/hello.world/.test('hello\nworld'));  // false 
console.log(/hello[\s\S]world/.test('hello\nworld'));  // true
console.log(/hello.world/s.test('hello\nworld')); // true
複製代碼

關於 . 標記,順便一提的是:

/^.$/.test('😀') //false,並不將 emoji 識別爲一個字符

/^.$/u.test('😀') //true,經過 u 修正
複製代碼

參考資料:



--End--

搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索