本章說明 ECMAScript 6 中新的正規表示式功能。如果您熟悉 ES5 正規表示式功能和 Unicode,將有所幫助。如有必要,請參閱「Speaking JavaScript」的以下兩章
/y
(黏著)
RegExp.prototype.exec(str)
RegExp.prototype.test(str)
String.prototype.search(regex)
String.prototype.match(regex)
String.prototype.split(separator, limit)
String.prototype.replace(search, replacement)
/u
(unicode)
.
)比對碼點,而不是碼元flags
RegExp()
可用作複製建構函式
exec()
的可迭代版本
ECMAScript 6 中有以下新的正規表示式功能
/y
(黏著)將正規表示式的每個比對錨定到前一個比對的結尾。/u
(unicode)將代理對(例如 \uD83D\uDE80
)視為碼點處理,並讓您在正規表示式中使用 Unicode 碼點跳脫(例如 \u{1F680}
)。flags
可讓你存取正規表達式的旗標,就像 source
已經可讓你存取 ES5 中的樣式
> /abc/ig.source // ES5
'abc'
> /abc/ig.flags // ES6
'gi'
RegExp()
來複製正規表達式
> new RegExp(/abc/ig).flags
'gi'
> new RegExp(/abc/ig, 'i').flags // change flags
'i'
/y
(黏著) 新的旗標 /y
在將正規表達式 re
與字串比對時會變更兩件事
re.lastIndex
:比對必須從 re.lastIndex
(前一個比對後的索引)開始。此行為類似於 ^
錨定,但使用該錨定時,比對必須總是從索引 0 開始。re.lastIndex
設定為比對後的索引。此行為類似於 /g
旗標。與 /g
一樣,/y
通常用於多次比對。此比對行為的主要使用案例是進行標記化,你希望每個比對緊接在它的前一個比對之後。稍後將提供透過黏著正規表達式和 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 (迴圈) | 索引 |
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
相同。
RegExp.prototype.test(str)
test()
的運作方式與 exec()
相同,但當比對成功或失敗時,它會傳回 true
或 false
(而不是比對物件或 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
String.prototype.search(regex)
search()
會忽略旗標 /g
和 lastIndex
(也不會變更)。它會從字串的開頭開始尋找第一個比對,並傳回其索引(如果沒有比對,則傳回 -1
)
const
REGEX
=
/a/
;
REGEX
.
lastIndex
=
2
;
// ignored
console
.
log
(
'xaxa'
.
search
(
REGEX
));
// 1
如果您設定旗標 /y
,lastIndex
仍然會被忽略,但正規表示式現在會錨定在索引 0。
const
REGEX
=
/a/y
;
REGEX
.
lastIndex
=
1
;
// ignored
console
.
log
(
'xaxa'
.
search
(
REGEX
));
// -1 (no match)
String.prototype.match(regex)
match()
有兩種模式
/g
,它的運作方式與 exec()
相同。/g
,它會傳回一個包含比對字串部分的陣列,或 null
。如果沒有設定旗標 /g
,match()
會像 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']
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)
[ '', '#', '', '#', '' ]
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」以取得詳細資訊。
黏著式符合的主要使用案例是代幣化,將文字轉換為代幣序列。關於代幣化的重要特點之一,是代幣是文字的片段,且它們之間不能有間隙。因此,黏著式符合在此非常完美。
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' ]
黏著式符合在代幣化期間的行為有助於錯誤處理。
如果你想要手動實作黏著式符合,你可以這樣做:函式 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
;
}
/u
(unicode) 標記 /u
會為正規表示式開啟特殊 Unicode 模式。該模式有兩個特點
\u{1F42A}
,透過碼點指定字元。一般的 Unicode 跳脫,例如 \u03B1
,只有四個十六進位數字的範圍(等於基本多文種平面)。Unicode 章節中的區段 有更多關於跳脫序列的資訊。接下來我將說明功能 2 的後果。我使用兩個 UTF-16 碼元(例如 \uD83D\uDE80
),而不是 Unicode 碼點跳脫(例如 \u{1F680}
)。這清楚說明代理對在 Unicode 模式中分組,並在 Unicode 模式和非 Unicode 模式中運作。
> '\u{1F680}' === '\uD83D\uDE80' // code point vs. surrogate pairs
true
在非 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
在 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
.
) 配對碼點,而不是碼元 在 Unicode 模式中,點運算子配對碼點(一個或兩個碼元)。在非 Unicode 模式中,它配對單一碼元。例如
> '\uD83D\uDE80'.match(/./gu).length
1
> '\uD83D\uDE80'.match(/./g).length
2
在 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
flags
在 ECMAScript 6 中,正規表示式具有下列資料屬性
source
flags
global
、ignoreCase
、multiline
、sticky
、unicode
lastIndex
順帶一提,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'
);
}
下一節說明建立已修改正則表達式副本的另一種方法。
RegExp()
可用作複製建構函式 在 ES6 中,建構函式 RegExp()
有兩個變體(第二個為新增變體)
new RegExp(pattern : string, flags = '')
pattern
所指定。如果未指定 flags
,會使用空字串 ''
。new RegExp(regex : RegExp, flags = regex.flags)
regex
。如果提供 flags
,則會決定副本的旗標。下列互動示範後一個變體
> 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'
);
}
exec()
的可迭代版本 下列函式 execAll()
為 exec()
的可迭代版本,修正了使用 exec()
擷取正則表達式所有配對的幾個問題
exec()
直到它傳回 null
)。exec()
會變異正則表達式,這表示副作用可能會變成問題。/g
。否則,只會傳回第一個配對。
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
下列字串方法現在會將部分工作委派給正則表達式方法
String.prototype.match
會呼叫 RegExp.prototype[Symbol.match]
。String.prototype.replace
會呼叫 RegExp.prototype[Symbol.replace]
。String.prototype.search
會呼叫 RegExp.prototype[Symbol.search]
。String.prototype.split
會呼叫 RegExp.prototype[Symbol.split]
。如需更多資訊,請參閱字串章節中的「委派正則表達式工作給其參數的字串方法」一節。