整體而言,ES6對於新特性的討論,多半集中在語法、資料結構,在規則表示式(Regular expression)也有實用新特性,部分新規範雖然是在ECMAScript 2018(ES9)提案,然而,基於V8的環境如Chrome與Node.js,已提早實作。

隨性的規則表示式支援

表面上看來,在JavaScript中使用規則表示式是簡單的,例如,有專用的規則表示式字面值(Regular expression literal),在String上,提供了match、search、split、replace等方法,也可以運用RegExp實例,以字串組合方式來動態建構規則表示式,然後……就沒有別的特點了!

JavaScript不單是語法設計上很隨性,在規則表示式API上,也是如此,像是有些方法,會因傳入的表示式旗標設定不同,而有不同的行為。例如,String沒有replaceAll方法,若想進行全部取代,在呼叫replace時傳入的表示式,必須設置全域旗標g;在match方法的部份,如果有符合的字串會傳回陣列,第一個位置(索引 0)是符合的字串,而各分組捕捉到的值,逐一放在後續索引處,但是,若傳入的規則表示式有全域旗標,傳回陣列中各元素會是符合表示式的值,並不會包含分組的部份。

RegExp實例本身的設計上,也有詭譎之處,它的狀態是可變的(Mutable),因而會有類似以下的怪異結果──明明就是相同字串,第一個測試顯示true,下一個卻顯示false:

const regex1 = new RegExp('foo*', 'g');
console.log(regex1.test('table football'));
console.log(regex1.test('table football'));

原因在於,若規則表示式被設置了全域旗標,RegExp實例的lastIndex就會有作用,預設值是0,test方法會以lastIndex作為測試的起點,如果比對到字串,lastIndex就會設為比對到的字串後之索引,這會是下個測試起點;如果沒有符合的字串,lastIndex會回到 0。因為這個特性,想找出一個字串中全部符合規則表示式的子字串,就會有底下的方式:

const regex1 = /((\d{4})-(\d{6}))/g;
var matched;
while((matched = regex1.exec('0970-666888, 0970-168168')) != null) {
console.log(`${matched[0]} found. The lastIndex is ${regex1.lastIndex}.`);
}

乍看這樣的程式碼,真不知為何能運作,其實,是每次比對後,lastIndex都會更新,而這行為是隱含的,相較其他語言對規則表示式API的支援,並不尋常。一般為了效率,代表規則表示式的實例因可能重用,基本上會設計為不可變動(Immutable),而在JavaScript中,卻要留意狀態的問題。

ES6規則表示式特性

根據維基百科條目,JavaScript是在1999年支援規則表示式,因而RegExp實例的狀態可變。或許因為如此,ES6新增的黏性匹配(sticky match)旗標y,也是基於lastIndex而運作,相關方法是從字串lastIndex索引後開始匹配,y被稱為黏性的原因也在於此。例如,底下範例首次比對後,r的lastIndex為2,也就是剩餘字串為_xx,然而表示式為x+,再次比對時,會因索引2處的字元為_而匹配失敗:

const text = 'xx_xx';
const r = /x+/y;
r.exec(text); // [ 'xx', index: 0, input: 'xx_xx' ]
r.exec(text); // null

ES6也新增了u旗標,可將\u{...}視為Unicode碼點來匹配,例如,'xyz林123'.replace(/\u{6797}/u, 'Lin')會傳回'xyzLin123';ES6也在RegExp上新增了sticky、unicode特性,用來測試RegExp實例是否被設定了相關旗標,或者,也可以透過新增的flags,來知道被設定的全部旗標。

另外,在String上,有split、search、replace與match方法,在ES6後,RegExp本身也定義這些方法,不過,須以符號(Symbol)來取得,也就是Symbol.split、Symbol.search、Symbol.replace與Symbol.match,例如,RegExp.prototype[Symbol.split]可取得定義在RegExp上的split方法,ES6在實作上,將String的split、search、replace與match方法,也全都委託給傳入的RegExp實例所對應的方法。

這麼做的原因之一,是與規則表示式相關的方法,都可以集中在RegExp上了,必要時,也可以繼承RegExp來重新定義相關的方法。實際上,由於JavaScript是動態定型言,這表示在執行String的split等方法時,並不一定要傳入RegExp實例,只要是具有 Symbol.split等對應協定的物件,就可以了;於是,也就有了機會,將一些外部的規則表示式程式庫,安插至String的規則表示式相關操作方法之中。

就像在〈RegExp的Symbol協定〉就有個例子,將XRegExp安插至傳入replace方法的物件,從而得到規則表示式命名捕捉群組(Named Capture Groups)的功能,實際上,這樣的機制,需到了ECMAScript 2018(ES9),才有所謂的〈RegExp Named Capture Groups〉提案。

ES9規則表示式特性

規則表示式能夠對子表示式進行分組,要捕捉的分組數量眾多時,若以號碼來區別,並不方便,這時若語言或工具支援,可為分組命名,之後就能透過名稱的形式來取用分組。

例如,使用(?<user>^[a-zA-Z]+\\d*)@(?<preCom>[a-z]+?.)com,在replace時,就可以使用$<user>@$<preCom>cc的形式,而不是$1@$2cc的形式,若在同一個表示式當中要參考分組,可以使用\k<user>的形式。然而,目前的瀏覽器與Node.js等實作,尚不支援命名捕捉群組,只能透過第三方程式庫,例如XRegExp。

而就規範來說,JavaScript的規則表示式,在撰寫方式上,目前只支援Lookahead與Negative lookahead,也就是想比對出的對象,之後要跟隨或不跟隨著特定文字的話,可使用(?=…)或(?!…),例如,\w+\s(?=Lin)這段比對文字規則的最後,必須有「Lin」,然而捕捉到的字串值並不包括「Lin」,像是針對「Justin Lin」的話,會抓取到「Justin」 。

相對地,如果想比對出的對象,前面必須有或沒有特定的文字,此時,我們可以使用(?<=…)或(?<!…),分別稱為Lookbehind與Negative Lookbehind。例如,(?<=data)-\w+會比對文字前是否有data字眼,然而捕捉到的字串值不包括data。實際上,JavaScript是在ES9中才有提案〈RegExp Lookbehind Assertions〉(https://goo.gl/2H3Tny),然而,基於新版V8引擎的Chrome與Node.js已經先實作,來提供支援了。

在規則表示式中,「.」表示任意字元,卻不包含換行及多位元組字元。對於多位元組字元,ES6可使用u旗標,而ES9會有個s旗標表示dotAll,設置s旗標後,「.」可以比對換行等全部字元,RegExp實例上,也將有個dotAll特性,可測試是否具有s旗標。

留意新特性的細節

或許是因為JavaScript一開始是在前端發展起來,使用規則表示式的情境相較於其他語言來說,較不複雜,因而規則表示式方面的資源並不豐富,相對來說,相關的文件都是比較舊的,然而,對於必須更深入運用規則表示式的人而言,可以留意一下ES6之後的新特性。

若需要深入認識ES6中的表示式新特性,〈New regular expression features in ECMAScript 6〉是份不錯的文件,而〈Regular Expressions in a post-ES6 world〉中,則包含ES9規範的表示式新特性介紹。

專欄作者

熱門新聞

Advertisement