23. 新的正規表示式功能
目錄
請支持本書:購買它(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖。)

23. 新的正規表示式功能

本章說明 ECMAScript 6 中新的正規表示式功能。如果您熟悉 ES5 正規表示式功能和 Unicode,將有所幫助。如有必要,請參閱「Speaking JavaScript」的以下兩章



23.1 概述

ECMAScript 6 中有以下新的正規表示式功能

23.2 新的旗標 /y(黏著)

新的旗標 /y 在將正規表達式 re 與字串比對時會變更兩件事

此比對行為的主要使用案例是進行標記化,你希望每個比對緊接在它的前一個比對之後。稍後將提供透過黏著正規表達式和 exec() 進行標記化的範例。

讓我們看看各種正規表達式運算如何對 /y 旗標做出反應。下表提供概觀。稍後我將提供更多詳細資料。

正規表達式的函式(re 是呼叫函式的正規表達式)

  旗標 開始比對 錨定至 比對結果 未比對 re.lastIndex
exec() 0 比對物件 null 不變
  /g re.lastIndex 比對物件 null 比對後的索引
  /y re.lastIndex re.lastIndex 比對物件 null 比對後的索引
  /gy re.lastIndex re.lastIndex 比對物件 null 比對後的索引
test() (任何) (類似 exec()) (類似 exec()) true false (類似 exec())

字串的函式(str 是呼叫函式的字串,r 是正規表達式參數)

  旗標 開始比對 錨定至 比對結果 未比對 r.lastIndex
search() –, /g 0 比對索引 -1 不變
  /y, /gy 0 0 比對索引 -1 不變
match() 0 比對物件 null 不變
  /y r.lastIndex r.lastIndex 比對物件 null 比對後的
            索引
  /g 前一個之後 包含比對的陣列 null 0
    match (迴圈)        
  /gy 前一個之後 前一個之後 包含比對的陣列 null 0
    match (迴圈) 索引      
split() –, /g 前一個之後 包含字串的陣列 [str] 不變
    match (迴圈)   比對之間    
  /y, /gy 前一個之後 前一個之後 包含空字串的陣列 [str] 不變
    match (迴圈) 索引 比對之間    
replace() 0 取代第一個比對 無取代 不變
  /y 0 0 取代第一個比對 無取代 不變
  /g 前一個之後 取代所有比對 無取代 不變
    match (迴圈)        
  /gy 前一個之後 前一個之後 取代所有比對 無取代 不變
    match (迴圈) 索引      

23.2.1 RegExp.prototype.exec(str)

如果未設定 /g,比對總是從開頭開始,但會跳過直到找到比對。REGEX.lastIndex 沒有變更。

const REGEX = /a/;

REGEX.lastIndex = 7; // ignored
const match = REGEX.exec('xaxa');
console.log(match.index); // 1
console.log(REGEX.lastIndex); // 7 (unchanged)

如果設定 /g,比對會從 REGEX.lastIndex 開始,並跳過直到找到比對為止。REGEX.lastIndex 會設定為比對後的位址。這表示如果您迴圈執行 exec() 直到傳回 null,您就會收到所有比對。

const REGEX = /a/g;

REGEX.lastIndex = 2;
const match = REGEX.exec('xaxa');
console.log(match.index); // 3
console.log(REGEX.lastIndex); // 4 (updated)

// No match at index 4 or later
console.log(REGEX.exec('xaxa')); // null

如果只設定 /y,比對會從 REGEX.lastIndex 開始,並錨定在該位址(不會跳過直到找到比對為止)。REGEX.lastIndex 的更新方式與設定 /g 時類似。

const REGEX = /a/y;

// No match at index 2
REGEX.lastIndex = 2;
console.log(REGEX.exec('xaxa')); // null

// Match at index 3
REGEX.lastIndex = 3;
const match = REGEX.exec('xaxa');
console.log(match.index); // 3
console.log(REGEX.lastIndex); // 4

同時設定 /y/g 與只設定 /y 相同。

23.2.2 RegExp.prototype.test(str)

test() 的運作方式與 exec() 相同,但當比對成功或失敗時,它會傳回 truefalse(而不是比對物件或 null

const REGEX = /a/y;

REGEX.lastIndex = 2;
console.log(REGEX.test('xaxa')); // false

REGEX.lastIndex = 3;
console.log(REGEX.test('xaxa')); // true
console.log(REGEX.lastIndex); // 4

23.2.3 String.prototype.search(regex)

search() 會忽略旗標 /glastIndex(也不會變更)。它會從字串的開頭開始尋找第一個比對,並傳回其索引(如果沒有比對,則傳回 -1

const REGEX = /a/;

REGEX.lastIndex = 2; // ignored
console.log('xaxa'.search(REGEX)); // 1

如果您設定旗標 /ylastIndex 仍然會被忽略,但正規表示式現在會錨定在索引 0。

const REGEX = /a/y;

REGEX.lastIndex = 1; // ignored
console.log('xaxa'.search(REGEX)); // -1 (no match)

23.2.4 String.prototype.match(regex)

match() 有兩種模式

如果沒有設定旗標 /gmatch() 會像 exec() 一樣擷取群組

{
    const REGEX = /a/;

    REGEX.lastIndex = 7; // ignored
    console.log('xaxa'.match(REGEX).index); // 1
    console.log(REGEX.lastIndex); // 7 (unchanged)
}
{
    const REGEX = /a/y;

    REGEX.lastIndex = 2;
    console.log('xaxa'.match(REGEX)); // null

    REGEX.lastIndex = 3;
    console.log('xaxa'.match(REGEX).index); // 3
    console.log(REGEX.lastIndex); // 4
}

如果只設定旗標 /g,則 match() 會在陣列中傳回所有比對的子字串(或 null)。比對總會從位置 0 開始。

const REGEX = /a|b/g;
REGEX.lastIndex = 7;
console.log('xaxb'.match(REGEX)); // ['a', 'b']
console.log(REGEX.lastIndex); // 0

如果您另外設定旗標 /y,則比對仍然會重複執行,同時將正規表示式錨定在先前比對後的索引(或 0)。

const REGEX = /a|b/gy;

REGEX.lastIndex = 0; // ignored
console.log('xab'.match(REGEX)); // null
REGEX.lastIndex = 1; // ignored
console.log('xab'.match(REGEX)); // null

console.log('ab'.match(REGEX)); // ['a', 'b']
console.log('axb'.match(REGEX)); // ['a']

23.2.5 String.prototype.split(separator, limit)

split() 的完整細節 在 Speaking JavaScript 中有說明

對於 ES6,如果你使用標記 /y,會看到事情如何改變,這很有趣。

使用 /y 時,字串必須以分隔符號開頭

> 'x##'.split(/#/y) // no match
[ 'x##' ]
> '##x'.split(/#/y) // 2 matches
[ '', '', 'x' ]

後續分隔符號僅在緊接第一個分隔符號後才會被辨識

> '#x#'.split(/#/y) // 1 match
[ '', 'x#' ]
> '##'.split(/#/y) // 2 matches
[ '', '', '' ]

這表示第一個分隔符號之前的字串和分隔符號之間的字串永遠是空的。

和往常一樣,你可以使用群組將分隔符號的部分放入結果陣列中

> '##'.split(/(#)/y)
[ '', '#', '', '#', '' ]

23.2.6 String.prototype.replace(search, replacement)

沒有標記 /g 時,replace() 僅取代第一個符合項

const REGEX = /a/;

// One match
console.log('xaxa'.replace(REGEX, '-')); // 'x-xa'

如果只設定 /y,你最多也會只得到一個符合項,但該符合項永遠會錨定在字串開頭。lastIndex 會被忽略且不變更。

const REGEX = /a/y;

// Anchored to beginning of string, no match
REGEX.lastIndex = 1; // ignored
console.log('xaxa'.replace(REGEX, '-')); // 'xaxa'
console.log(REGEX.lastIndex); // 1 (unchanged)

// One match
console.log('axa'.replace(REGEX, '-')); // '-xa'

設定 /g 時,replace() 會取代所有符合項

const REGEX = /a/g;

// Multiple matches
console.log('xaxa'.replace(REGEX, '-')); // 'x-x-'

設定 /gy 時,replace() 會取代所有符合項,但每個符合項會錨定在前一個符合項的結尾

const REGEX = /a/gy;

// Multiple matches
console.log('aaxa'.replace(REGEX, '-')); // '--xa'

參數 replacement 也可以是函式,請參閱「Speaking JavaScript」以取得詳細資訊

23.2.7 範例:使用黏著式符合進行代幣化

黏著式符合的主要使用案例是代幣化,將文字轉換為代幣序列。關於代幣化的重要特點之一,是代幣是文字的片段,且它們之間不能有間隙。因此,黏著式符合在此非常完美。

function tokenize(TOKEN_REGEX, str) {
    const result = [];
    let match;
    while (match = TOKEN_REGEX.exec(str)) {
        result.push(match[1]);
    }
    return result;
}

const TOKEN_GY = /\s*(\+|[0-9]+)\s*/gy;
const TOKEN_G  = /\s*(\+|[0-9]+)\s*/g;

在合法的代幣序列中,黏著式符合和非黏著式符合會產生相同的輸出

> tokenize(TOKEN_GY, '3 + 4')
[ '3', '+', '4' ]
> tokenize(TOKEN_G, '3 + 4')
[ '3', '+', '4' ]

不過,如果字串中有非代幣文字,黏著式符合會停止代幣化,而非黏著式符合會略過非代幣文字

> tokenize(TOKEN_GY, '3x + 4')
[ '3' ]
> tokenize(TOKEN_G, '3x + 4')
[ '3', '+', '4' ]

黏著式符合在代幣化期間的行為有助於錯誤處理。

23.2.8 範例:手動實作黏著式符合

如果你想要手動實作黏著式符合,你可以這樣做:函式 execSticky() 的作用類似於黏著模式下的 RegExp.prototype.exec()

 function execSticky(regex, str) {
     // Anchor the regex to the beginning of the string
     let matchSource = regex.source;
     if (!matchSource.startsWith('^')) {
         matchSource = '^' + matchSource;
     }
     // Ensure that instance property `lastIndex` is updated
     let matchFlags = regex.flags; // ES6 feature!
     if (!regex.global) {
         matchFlags = matchFlags + 'g';
     }
     const matchRegex = new RegExp(matchSource, matchFlags);

     // Ensure we start matching `str` at `regex.lastIndex`
     const matchOffset = regex.lastIndex;
     const matchStr = str.slice(matchOffset);
     let match = matchRegex.exec(matchStr);

     // Translate indices from `matchStr` to `str`
     regex.lastIndex = matchRegex.lastIndex + matchOffset;
     match.index = match.index + matchOffset;
     return match;
 }

23.3 新標記 /u(unicode)

標記 /u 會為正規表示式開啟特殊 Unicode 模式。該模式有兩個特點

  1. 你可以使用 Unicode 碼點跳脫序列,例如 \u{1F42A},透過碼點指定字元。一般的 Unicode 跳脫,例如 \u03B1,只有四個十六進位數字的範圍(等於基本多文種平面)。
  2. 正規表示式模式和字串中的「字元」是碼點(不是 UTF-16 碼元)。碼元會轉換成碼點。

Unicode 章節中的區段 有更多關於跳脫序列的資訊。接下來我將說明功能 2 的後果。我使用兩個 UTF-16 碼元(例如 \uD83D\uDE80),而不是 Unicode 碼點跳脫(例如 \u{1F680})。這清楚說明代理對在 Unicode 模式中分組,並在 Unicode 模式和非 Unicode 模式中運作。

> '\u{1F680}' === '\uD83D\uDE80' // code point vs. surrogate pairs
true

23.3.1 後果:正規表示式中的單獨代理僅配對單獨代理

在非 Unicode 模式中,正規表示式中的單獨代理甚至會在(代理對編碼)碼點中找到

> /\uD83D/.test('\uD83D\uDC2A')
true

在 Unicode 模式中,代理對會變成原子單位,而單獨代理不會在其中「內部」找到

> /\uD83D/u.test('\uD83D\uDC2A')
false

實際的單獨代理仍會找到

> /\uD83D/u.test('\uD83D \uD83D\uDC2A')
true
> /\uD83D/u.test('\uD83D\uDC2A \uD83D')
true

23.3.2 後果:您可以在字元類別中放置碼點

在 Unicode 模式中,您可以將碼點放入字元類別中,而且它們不再會被解釋為兩個字元。

> /^[\uD83D\uDC2A]$/u.test('\uD83D\uDC2A')
true
> /^[\uD83D\uDC2A]$/.test('\uD83D\uDC2A')
false

> /^[\uD83D\uDC2A]$/u.test('\uD83D')
false
> /^[\uD83D\uDC2A]$/.test('\uD83D')
true

23.3.3 後果:點運算子 (.) 配對碼點,而不是碼元

在 Unicode 模式中,點運算子配對碼點(一個或兩個碼元)。在非 Unicode 模式中,它配對單一碼元。例如

> '\uD83D\uDE80'.match(/./gu).length
1
> '\uD83D\uDE80'.match(/./g).length
2

23.3.4 後果:量詞套用於碼點,而不是碼元

在 Unicode 模式中,量詞套用於碼點(一個或兩個碼元)。在非 Unicode 模式中,它們套用於單一碼元。例如

> /\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

23.4 新的資料屬性flags

在 ECMAScript 6 中,正規表示式具有下列資料屬性

順帶一提,lastIndex 現在是唯一的實例屬性,所有其他資料屬性都透過內部實例屬性和 getter 實作,例如 get RegExp.prototype.global

屬性 source(在 ES5 中已存在)包含正則表達式模式,為字串

> /abc/ig.source
'abc'

屬性 flags 為新增屬性,包含旗標,為字串,每個旗標一個字元

> /abc/ig.flags
'gi'

無法變更現有正則表達式的旗標(ignoreCase 等一直都是不可變的),但 flags 允許您建立旗標已變更的副本

function copyWithIgnoreCase(re) {
    return new RegExp(re.source,
        re.flags.includes('i') ? re.flags : re.flags+'i');
}

下一節說明建立已修改正則表達式副本的另一種方法。

23.5 RegExp() 可用作複製建構函式

在 ES6 中,建構函式 RegExp() 有兩個變體(第二個為新增變體)

下列互動示範後一個變體

> new RegExp(/abc/ig).flags
'gi'
> new RegExp(/abc/ig, 'i').flags // change flags
'i'

因此,RegExp 建構函式提供我們變更旗標的另一種方式

function copyWithIgnoreCase(re) {
    return new RegExp(re,
        re.flags.includes('i') ? re.flags : re.flags+'i');
}

23.5.1 範例:exec() 的可迭代版本

下列函式 execAll()exec() 的可迭代版本,修正了使用 exec() 擷取正則表達式所有配對的幾個問題

function* execAll(regex, str) {
    // Make sure flag /g is set and regex.index isn’t changed
    const localCopy = copyAndEnsureFlag(regex, 'g');
    let match;
    while (match = localCopy.exec(str)) {
        yield match;
    }
}
function copyAndEnsureFlag(re, flag) {
    return new RegExp(re,
        re.flags.includes(flag) ? re.flags : re.flags+flag);
}

使用 execAll()

const str = '"fee" "fi" "fo" "fum"';
const regex = /"([^"]*)"/;

// Access capture of group #1 via destructuring
for (const [, group1] of execAll(regex, str)) {
    console.log(group1);
}
// Output:
// fee
// fi
// fo
// fum

23.6 委派給正則表達式方法的字串方法

下列字串方法現在會將部分工作委派給正則表達式方法

如需更多資訊,請參閱字串章節中的「委派正則表達式工作給其參數的字串方法」一節。

下一頁:24. 非同步程式設計(背景)