RegExp 實例

July 11, 2022

規則表示式字面值實際上會生成 RegExp 實例,也可以直接建構 RegExp 實例:

> 'Justin1Monica2Irene'.split(new RegExp('\\d'))
[ 'Justin', 'Monica', 'Irene' ]

建構 RegExp

使用建構式 RegExp 時必須指定字串,如果事先能決定表示式內容,使用字面值方式比較方便,若必須以動態地方式建構規則表示式,則可以使用字串方式來組合,然而必須注意到規則表示式中的一些字元,書寫為字串時必須轉譯(Escape)的問題。

例如,若有個字串是 'Justin+Monica+Irene',想使用 split 方法依 + 切割,要使用的規則表示式是 \+,要將 \+ 放至 '' 之間時,必須忽略 \+\,因而要撰寫為 '\\+'

> 'Justin+Monica+Irene'.split(new RegExp('\\+'))
[ 'Justin', 'Monica', 'Irene' ]

類似地,如果有個字串是 'Justin||Monica||Irene',想使用 split 方法依 || 切割,要使用的規則表示式是 \|\|,要將 \|\| 放至 ''之間時,必須忽略 \|\,就必須撰寫為 '\\|\\|'

> 'Justin||Monica||Irene'.split(new RegExp('\\|\\|'))
[ 'Justin', 'Monica', 'Irene' ]

如果有個字串是 'Justin\\Monica\\Irene',也就是原始文字是 Justin\Monica\Irene 的字串表示,若想使用 split 方法依 \ 切割,要使用的規則表示式是 \\,那就得如下撰寫:

> 'Justin\\Monica\\Irene'.split(new RegExp('\\\\'))
[ 'Justin', 'Monica', 'Irene' ]

旗標設置

在使用建構式 RegExp 時,可以在第二個參數處指定旗標。例如:

> 'xfooxxxxxxfoo'.replace(new RegExp('.*?foo', 'g'), 'Orz')
'OrzOrz'

在建構 RegExp 時,也可以指定既有的 RegExp 實例,例如:

const regex = new RegExp(/.*?foo/g); // 相當於 new RegExp('.*?foo', 'g')

在 ES6 中,還可以使用底下的方式(在只支援 ES5 環境中不行):

const regex = new RegExp(/.*?foo/, 'g');

RegExptest 方法,可用來搜尋、測試字串中是否有子字串符合規則表示式。例如:

> const regex1 = new RegExp('foo*');
undefined
> console.log(regex1.test('table football'));
true
undefined

規則表示式實際上也是門語言,因而建立 RegExp 也是需要經過剖析到轉換至內部呈現格式等數個階段,為了效率考量,在可行的情況下,可重用已建立的 RegExp

變動性

然而,JavaScript 的 RegExp 實際上是狀態可變的(Mutable),例如,在〈JavaScript 字串與規則表示式〉,就看過可修改 lastIndex 的值(為了示範黏性匹配)。

> const re = /.*?foo/y;
undefined
> re.lastIndex = 4;
4
> 'xfooxxxxxxfoo'.replace(re, 'Orz')
'xfooOrz'

一個代表規則表示式的物件是狀態可變的,就 API 設計上來說並不尋常,例如,若規則表示式被設置了全域旗標,lastIndex 就會有作用,預設值是 0,test 方法這時會以 lastIndex 作為測試的起點,如果比對到字串,lastIndex 就會設為比對到的字串後之索引,作為下個測試起點,如果沒有符合的字串,lastIndex 會回到 0。

因此,這就會有以下的怪異結果,明明就是相同字串,一個顯示 true,下一個卻顯示 false

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

RegExp 有個 exec 方法,類似 Stringmatch 方法,如果有符合的字串,它會傳回一個陣列(否則傳回 null),第一個位置(索引 0)是符合的字串,而各分組捕捉到的值,逐一放在後續索引處。

如果分組使用了 (?:...) 就不會捕捉,傳回的陣列中也就不會有該分組的值。

> /(\d{4})-(\d{6})/.exec('0970-666888');
[ '0970-666888', '0970', '666888', index: 0, input: '0970-666888' ]
> /(?:\d{4})-(?:\d{6})/.exec('0970-666888');
[ '0970-666888', index: 0, input: '0970-666888' ]
> /(?:(\d{4})-(\d{6}))/.exec('0970-666888');
[ '0970-666888', '0970', '666888', index: 0, input: '0970-666888' ]

match 不同的是,如果加上了全域旗標,那麼 exec 傳回的陣列,並不會是全部符合表示式的值:

> /((\d{4})-(\d{6}))/g.exec('0970-666888, 0970-168168');
[ '0970-666888',
    '0970-666888',
    '0970',
    '666888',
    index: 0,
    input: '0970-666888, 0970-168168' ]
> /((\d{4})-(\d{6}))/g.exec('0970-666888, 0970-168168');
[ '0970-666888',
    '0970-666888',
    '0970',
    '666888',
    index: 0,
    input: '0970-666888, 0970-168168' ]
> /((\d{4})-(\d{6}))/g.exec('0970-666888, 0970-168168');
[ '0970-666888',
    '0970-666888',
    '0970',
    '666888',
    index: 0,
    input: '0970-666888, 0970-168168' ]

蠻奇怪的不是嗎?稍微改一下,結果令人驚奇:

> const regex1 = /((\d{4})-(\d{6}))/g;
undefined
> regex1.exec('0970-666888, 0970-168168')
[ '0970-666888',
    '0970-666888',
    '0970',
    '666888',
    index: 0,
    input: '0970-666888, 0970-168168' ]
> regex1.exec('0970-666888, 0970-168168')
[ '0970-168168',
    '0970-168168',
    '0970',
    '168168',
    index: 13,
    input: '0970-666888, 0970-168168' ]
> regex1.exec('0970-666888, 0970-168168')
null

記得嗎?RegExp 實際上是狀態可變的(Mutable),當啟用了全域旗標,lastIndex 會在比對到字串時變動,作為下個比對時的索引起點,因此,為了使用 exec 找出一個字串中全部符合規則表示式的子字串,就會有底下的方式:

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

用這個方式來找出全部符合的字串,其實是蠻詭異的,只能說,JavaScript 不單是語法,連 API 都設計地很隨性吧!

分享到 LinkedIn 分享到 Facebook 分享到 Twitter