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');
RegExp
的 test
方法,可用來搜尋、測試字串中是否有子字串符合規則表示式。例如:
> 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
方法,類似 String
的 match
方法,如果有符合的字串,它會傳回一個陣列(否則傳回 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 都設計地很隨性吧!