正則表達式在 ES2018 中的新寫法

翻譯:瘋狂的技術宅
原文: https://www.smashingmagazine....

本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript


摘要:若是你曾用 JavaScript 作過複雜的文本處理和操做,那麼你將會對 ES2018 中引入的新功能愛不釋手。 在本文中,咱們將詳細介紹第 9 版標準如何提升 JavaScript 的文本處理能力。前端


有一個很好的理由可以解釋爲何大多數編程語言都支持正則表達式:它們是用於處理文本的極其強大的工具。 一般一行正則表達式代碼就能完成須要幾十行代碼才能搞定的文本處理任務。 雖然大多數語言中的內置函數足以對字符串進行通常的搜索和替換操做,但更加複雜的操做(例如驗證文本輸入)一般須要使用正則表達式。java

自從 1999 年推出 ECMAScript 標準第 3 版以來,正則表達式已成爲 JavaScript 語言的一部分。ECMAScript 2018(簡稱ES2018)是該標準的第 9 版,經過引入四個新功能進一步提升了JavaScript的文本處理能力:git

下面詳細介紹這些新功能。github

後行斷言

可以根據以後或以前的內容匹配一系列字符,使你能夠丟棄可能不須要的匹配。 當你須要處理大字符串而且意外匹配的可能性很高時,這個功能很是有用。 幸運的是,大多數正則表達式都爲此提供了 lookbehind 和 lookahead 斷言。正則表達式

在 ES2018 以前,JavaScript 中只提供了先行斷言。 lookahead 容許你在一個斷言模式後緊跟另外一個模式。express

先行斷言有兩種版本:正向和負向。 正向先行斷言的語法是 (?=...)。 例如,正則表達式 /Item(?= 10)/ 僅在後面跟隨有一個空格和數字 10 的時候才與 Item 匹配:編程

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]

此代碼使用 exec() 方法在字符串中搜索匹配項。 若是找到匹配項, exec() 將返回一個數組,其中第一個元素是匹配的字符串。 數組的 index 屬性保存匹配字符串的索引, input 屬性保存搜索執行的整個字符串。 最後,若是在正則表達式中使用了命名捕獲組,則將它們放在 groups 屬性中。 在代碼中, groups 的值爲 undefined ,由於沒有被命名的捕獲組。數組

負向先行的構造是 (?!...) 。 負向先行斷言的模式後面沒有特定的模式。 例如, /Red(?!head)/ 僅在其後不跟隨 head 時匹配 Red瀏覽器

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 爲 JavaScript 補充了後行斷言。 用 (?<=...) 表示,後行斷言容許你在一個模式前面存在另外一個模式時進行匹配。

假設你須要以歐元檢索產品的價格可是不捕獲歐元符號。 經過後行斷言,會使這項任務變得更加簡單:

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]

注意先行(Lookahead)和後行(lookbehind)斷言一般被稱爲「環視」(lookarounds)

後行斷言的反向版本由 (?<!...) 表示,使你可以匹配不在lookbehind中指定的模式以前的模式。 例如,正則表達式 /(?<!\d{3}) meters/ 會在 三個數字不在它以前 匹配單詞「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

與前行斷言同樣,你能夠連續使用多個後行斷言(負向或正向)來建立更復雜的模式。下面是一個例子:

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]

此正則表達式僅匹配包含「meters」的字符串,若是它前面緊跟 35 以外的任何兩個數字。正向後行確保模式前面有兩個數字,同時負向後行可以確保該數字不是 35。

命名捕獲組

你能夠經過將字符封裝在括號中的方式對正則表達式的一部分進行分組。 這能夠容許你將規則限制爲模式的一部分或在整個組中應用量詞。 此外你能夠經過括號來提取匹配值並進行進一步處理。

下列代碼給出瞭如何在字符串中查找帶有 .jpg 並提取文件名的示例:

const re = /(\w+)\.jpg/;
const str = 'File name: cat.jpg';
const match = re.exec(str);
const fileName = match[1];

// The second element in the resulting array holds the portion of the string that parentheses matched
console.log(match);
// → ["cat.jpg", "cat", index: 11, input: "File name: cat.jpg", groups: undefined]

console.log(fileName);
// → cat

在更復雜的模式中,使用數字引用組只會使自己就已經很神祕的正則表達式的語法更加混亂。 例如,假設你要匹配日期。 因爲在某些國家和地區會交換日期和月份的位置,所以會弄不清楚究竟哪一個組指的是月份,哪一個組指的是日期:

const re = /(\d{4})-(\d{2})-(\d{2})/;
const match = re.exec('2020-03-04');

console.log(match[0]);    // → 2020-03-04
console.log(match[1]);    // → 2020
console.log(match[2]);    // → 03
console.log(match[3]);    // → 04

ES2018針對此問題的解決方案名爲捕獲組,它使用更具表現力的 (?<name>...) 形式的語法:

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

console.log(match.groups);          // → {year: "2020", month: "03", day: "04"}
console.log(match.groups.year);     // → 2020
console.log(match.groups.month);    // → 03
console.log(match.groups.day);      // → 04

由於生成的對象可能會包含與命名組同名的屬性,因此全部命名組都在名爲 groups 的單獨對象下定義。

許多新的和傳統的編程語言中都存在相似的結構。 例如Python對命名組使用 (?P<name>) 語法。 Perl支持與 JavaScript 相同語法的命名組( JavaScript 已經模仿了 Perl 的正則表達式語法)。 Java也使用與Perl相同的語法。

除了可以經過 groups 對象訪問命名組以外,你還能夠用編號引用訪問組—— 相似於常規捕獲組:

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

console.log(match[0]);    // → 2020-03-04
console.log(match[1]);    // → 2020
console.log(match[2]);    // → 03
console.log(match[3]);    // → 04

新語法也適用於解構賦值:

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

即便正則表達式中不存在命名組,也始終建立 groups 對象:

const re = /\d+/;
const match = re.exec('123');

console.log('groups' in match);    // → true

若是可選的命名組不參與匹配,則 groups 對象仍將具備命名組的屬性,但該屬性的值爲 undefined

const re = /\d+(?<ordinal>st|nd|rd|th)?/;

let match = re.exec('2nd');

console.log('ordinal' in match.groups);    // → true
console.log(match.groups.ordinal);         // → nd

match = re.exec('2');

console.log('ordinal' in match.groups);    // → true
console.log(match.groups.ordinal);         // → undefined

你能夠稍後在模式中引用常規捕獲的組,並使用 \1 的形式進行反向引用。 例如如下代碼使用在行中匹配兩個字母的捕獲組,而後在模式中調用它:

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>/ 語法。 下面是一個例子:

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

此正則表達式在句子中查找連續的重複單詞。 若是你願意,還能夠用帶編號的後引用來調用命名的捕獲組:

const re = /\b(?<dup>\w+)\s+\1\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

也能夠同時使用帶編號的後引用和命名後向引用:

const re = /(?<digit>\d):\1:\k<digit>/;

const match = re.exec('5:5:5');        

console.log(match[0]);    // → 5:5:5

與編號的捕獲組相似,能夠將命名的捕獲組插入到 replace() 方法的替換值中。 爲此,你須要用到 $<name> 構造。 例如:

const str = 'War & Peace';

console.log(str.replace(/(War) & (Peace)/, '$2 & $1'));    
// → Peace & War

console.log(str.replace(/(?<War>War) & (?<Peace>Peace)/, '$<Peace> & $<War>'));    
// → Peace & War

若是要使用函數執行替換,則能夠引用命名組,方法與引用編號組的方式相同。 第一個捕獲組的值將做爲函數的第二個參數提供,第二個捕獲組的值將做爲第三個參數提供:

const str = 'War & Peace';

const result = str.replace(/(?<War>War) & (?<Peace>Peace)/, function(match, group1, group2, offset, string) {
    return group2 + ' & ' + group1;
});

console.log(result);    // → Peace & War

s (dotAll) Flag

默認狀況下,正則表達式模式中的點 (.) 元字符匹配除換行符 (\n) 和回車符 (\r)以外的全部字符:

console.log(/./.test('\n'));    // → false
console.log(/./.test('\r'));    // → false

儘管有這個缺點,JavaScript 開發者仍然能夠經過使用兩個相反的速記字符類來匹配全部字符,例如[ w W],它告訴正則表達式引擎匹配一個字符(\w)或非單詞字符(\W):

console.log(/[\w\W]/.test('\n'));    // → true
console.log(/[\w\W]/.test('\r'));    // → true

ES2018旨在經過引入 s (dotAll) 標誌來解決這個問題。 設置此標誌後,它會更改點 (.)元字符的行爲以匹配換行符:

console.log(/./s.test('\n'));    // → true
console.log(/./s.test('\r'));    // → true

s 標誌能夠在每一個正則表達式的基礎上使用,所以不會破壞依賴於點元字符的舊行爲的現有模式。 除了 JavaScript 以外, s 標誌還可用於許多其餘語言,如 Perl 和 PHP。

Unicode 屬性轉義

ES2015中引入的新功能包括Unicode感知。 可是即便設置了 u 標誌,速記字符類仍然沒法匹配Unicode字符。

請考慮如下案例:

const str = '𝟠';

console.log(/\d/.test(str));     // → false
console.log(/\d/u.test(str));    // → false

𝟠被認爲是一個數字,但 \d 只能匹配ASCII [0-9],所以 test() 方法返回 false。 由於改變速記字符類的行爲會破壞現有的正則表達式模式,因此決定引入一種新類型的轉義序列。

在ES2018中,當設置 u 標誌時,Unicode屬性轉義(由 \p{...} 表示)在正則表達式中可用。 如今要匹配任何Unicode 數字,你只需使用 \p{Number},以下所示:

const str = '𝟠';
console.log(/\p{Number}/u.test(str));     // → true

要匹配 Unicode 字符,你可使用\p{Alphabetic}

const str = '漢';

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

// the \w shorthand cannot match 漢
console.log(/\w/u.test(str));    // → false

\P{...}\p{...} 的否認版本,並匹配 \p{...} 沒有的全部字符:

console.log(/\P{Number}/u.test('𝟠'));    // → false
console.log(/\P{Number}/u.test('漢'));    // → true

console.log(/\P{Alphabetic}/u.test('𝟠'));    // → true
console.log(/\P{Alphabetic}/u.test('漢'));    // → false

當前規範提案中提供了受支持屬性的完整列表。

請注意,使用不受支持的屬性會致使 SyntaxError

console.log(/\p{undefined}/u.test('漢'));    // → SyntaxError

兼容性列表

桌面瀏覽器

Chrome Firefox Safari Edge
後行斷言 62 X X X
命名捕獲組 64 X 11.1 X
s (dotAll) Flag 62 X 11.1 X
Unicode 屬性轉義 64 X 11.1 X

移動瀏覽器

Chrome For Android Firefox For Android iOS Safari Edge Mobile Samsung Internet Android Webview
後行斷言 62 X X X 8.2 62
命名捕獲組 64 X 11.3 X X 64
s (dotAll) Flag 62 X 11.3 X 8.2 62
Unicode 屬性轉義 64 X 11.3 X X 64

NODE.JS

  • 8.3.0 (須要 --harmony 運行時標誌)
  • 8.10.0 (支持 s (dotAll) flag 和後行斷言)
  • 10.0.0 (徹底支持)

總結

經過使正則表達式獲得加強,ES2018 繼續了之前版本ECMAScript的工做。新功能包括後行斷言,命名捕獲組, s (dotAll) flag 和 Unicode屬性轉義。 後行斷言容許你在一個模式前面存在另外一個模式進行匹配。與常規捕獲組相比,命名捕獲組使用了更具表現力的語法。 s (dotAll) flag 經過更改點(.)元字符的行爲來匹配換行符。最後,Unicode 屬性轉義在正則表達式中提供了一種新類型的轉義序列。

在構建複雜的模式時,使用正則表達式測試程序一般頗有幫助。一個好的測試器會提供一個接口來對字符串的正則表達式進行測試,並顯示引擎所作的每一步,這在你理解其餘人編寫的表達式時很是有幫助。它還能夠檢測正則表達式中可能出現的語法錯誤。 Regex101 和 RegexBuddy 是兩個值得一試的正則表達式測試程序。

除此以外你能推薦其餘的工具嗎?歡迎在評論中分享!


本文首發微信公衆號:jingchengyideng

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

相關文章
相關標籤/搜索