RegExp
)/u
による Unicode モードregExp.test(str)
: 一致がありますか? [ES3]str.search(regExp)
: 一致はどのインデックスにありますか? [ES3]regExp.exec(str)
: キャプチャグループ [ES3]str.match(regExp)
: すべてのグループ 0 キャプチャを取得する [ES3]str.matchAll(regExp)
:取得所有符合物件的 iterable [ES2020]regExp.exec()
與 str.match()
與 str.matchAll()
str.replace()
和 str.replaceAll()
替換/g
和 /y
,以及屬性 .lastIndex
(進階)
/g
和 /y
/g
和 /y
確切如何影響函式?/g
和 /y
的四個陷阱以及如何處理.lastIndex
的使用案例:從特定索引開始比對.lastIndex
的缺點.global
(/g
) 和 .sticky
(/y
) 功能可用性
除非另有說明,否則每個正規表示式功能自 ES3 起即可使用。
建立正規表示式的兩種主要方法為
文字:在靜態編譯(載入時)。
/abc/ui
建構函式:在動態編譯(執行時)。
new RegExp('abc', 'ui')
兩種正規表示式都有相同的兩個部分
abc
– 實際的正規表示式。u
和 i
。旗標設定如何詮釋樣式。例如,i
啟用不區分大小寫的比對。可用旗標清單載於 本章稍後。RegExp()
建構函式有兩個變體
new RegExp(pattern : string, flags = '')
[ES3]
建立新的正規表示式,如 pattern
所指定。如果沒有 flags
,則使用空字串 ''
。
new RegExp(regExp : RegExp, flags = regExp.flags)
[ES6]
regExp
會被複製。如果提供了 flags
,則會決定複製項的旗標。
第二個變體適用於複製正規表示式,同時可以選擇修改它們。旗標是不可變的,這是變更它們的唯一方法,例如
function copyAndAddFlags(regExp, flagsToAdd='') {
// The constructor doesn’t allow duplicate flags;
// make sure there aren’t any:
const newFlags = Array.from(
new Set(regExp.flags + flagsToAdd)
.join('');
)return new RegExp(regExp, newFlags);
}.equal(/abc/i.flags, 'i');
assert.equal(copyAndAddFlags(/abc/i, 'g').flags, 'gi'); assert
在正規表示式的頂層,下列語法字元是特殊字元。它們會透過加上反斜線 (\
) 作為前綴來跳脫。
\ ^ $ . * + ? ( ) [ ] { } |
在正規表示式字串中,我們必須跳脫斜線
> /\//.test('/')true
在 new RegExp()
的引數中,我們不必跳脫斜線
> new RegExp('/').test('/')true
原子是正規表示式的基本組成元素。
^
、$
等) 的字元。樣式字元會與它們自己相符。範例:A b %
.
會與任何字元相符。我們可以使用旗標 /s
(dotAll
) 來控制點是否與行終止符相符 (詳情請見下方)。\f
:換頁 (FF)\n
:換行 (LF)\r
:回車 (CR)\t
:字元製表\v
:行製表\cA
(Ctrl-A)、…、\cZ
(Ctrl-Z)\u00E4
/u
):\u{1F44D}
\d
:數字 (與 [0-9]
相同)
\D
:非數字\w
:”字詞” 字元 (與 [A-Za-z0-9_]
相同,與程式語言中的識別碼相關)
\W
:非字詞字元\s
:空白 (空白、製表、行終止符等)
\S
:非空白\p{White_Space}
、\P{White_Space}
等。
/u
。在 Unicode 標準中,每個字元都有屬性,也就是描述它的元資料。屬性在定義字元的性質中扮演重要的角色。引用Unicode 標準,第 3.3 節,D3
字元的語義由其身分、規範屬性和行為決定。
以下列出一些屬性的範例
名稱
:一個由大寫字母、數字、連字號和空白組成的唯一名稱,例如
名稱 = 拉丁文大寫字母 A
🙂
:名稱 = 微笑臉
General_Category
:分類字元,例如
General_Category = 小寫字母
General_Category = 貨幣符號
White_Space
:用於標記不可見的空白字元,例如空白、標籤和換行符,例如
White_Space = True
White_Space = False
Age
:字元在 Unicode 標準中被引入的版本,例如:歐元符號 € 在 Unicode 標準的 2.1 版本中加入。
Age = 2.1
Block
:一個連續的碼點範圍。區塊不會重疊,且其名稱是唯一的。例如
Block = 基本拉丁文
(範圍 U+0000..U+007F)🙂
:Block = 表情符號
(範圍 U+1F600..U+1F64F)Script
:是由一個或多個書寫系統使用的字元集合。
Script = 希臘文
Script = 西里爾文
Unicode 屬性跳脫看起來像這樣
\p{prop=value}
:符合其屬性 prop
的值為 value
的所有字元。\P{prop=value}
:符合其屬性 prop
的值不為 value
的所有字元。\p{bin_prop}
:符合其二進制屬性 bin_prop
為 True 的所有字元。\P{bin_prop}
:符合其二進制屬性 bin_prop
為 False 的所有字元。註解
我們只能在設定 /u
旗標時使用 Unicode 屬性跳脫。沒有 /u
時,\p
與 p
相同。
如果屬性為 General_Category
,則可以使用格式 (3) 和 (4) 作為縮寫。例如,以下兩個跳脫是等效的
\p{Uppercase_Letter}
\p{General_Category=Uppercase_Letter}
範例
檢查空白
> /^\p{White_Space}+$/u.test('\t \n\r')true
檢查希臘字母
> /^\p{Script=Greek}+$/u.test('μετά')true
刪除任何字母
> '1π2ü3é4'.replace(/\p{Letter}/ug, '')'1234'
刪除小寫字母
> 'AbCdEf'.replace(/\p{Lowercase_Letter}/ug, '')'ACE'
進一步閱讀
字元類別以方括號包住類別範圍。類別範圍指定一組字元
[«類別範圍»]
符合集合中的任何字元。[^«類別範圍»]
符合集合中沒有的任何字元。類別範圍的規則
非語法字元代表它們自己:[abc]
只有以下四個字元是特殊字元,必須透過斜線跳脫
^ \ - ]
^
只有在它出現在第一個時才需要跳脫。-
如果出現在第一個或最後一個時不需要跳脫。字元跳脫 (\n
、\u{1F44D}
等) 具有通常的意義。
\b
代表退格鍵。在正規表示式的其他地方,它符合字詞邊界。字元類別跳脫 (\d
、\p{White_Space}
等) 具有通常的意義。
字元範圍透過連字符指定:[a-z]
(#+)
\1
、\2
等。(?<hashes>#+)
\k<hashes>
(?:#+)
預設情況下,所有下列量詞都是貪婪的(它們會符合盡可能多的字元)
?
:符合零次或一次*
:符合零次或多次+
:符合一次或多次{n}
:符合 n
次{n,}
:符合 n
次或多次{n,m}
:符合至少 n
次,最多 m
次。若要讓它們變成不貪婪的(讓它們符合盡可能少的字元),請在它們後面加上問號 (?
)
> /".*"/.exec('"abc"def"')[0] // greedy'"abc"def"'
> /".*?"/.exec('"abc"def"')[0] // reluctant'"abc"'
^
僅符合輸入的開頭$
僅符合輸入的結尾\b
僅符合字詞邊界
\B
僅符合不在字詞邊界時正前瞻: (?=«樣式»)
如果 樣式
符合接下來的內容,則符合。
範例:由小寫字母組成的序列,其後接一個 X
。
> 'abcX def'.match(/[a-z]+(?=X)/g)[ 'abc' ]
請注意,X
本身並非符合子字串的一部分。
負向先行斷言: (?!«pattern»)
如果 pattern
與後續內容不符,則會配對。
範例:不包含後接 X
的小寫字母序列。
> 'abcX def'.match(/[a-z]+(?!X)/g)[ 'ab', 'def' ]
正向後向斷言: (?<=«pattern»)
如果 pattern
與前述內容相符,則會配對。
範例:包含前接 X
的小寫字母序列。
> 'Xabc def'.match(/(?<=X)[a-z]+/g)[ 'abc' ]
負向後向斷言: (?<!«pattern»)
如果 pattern
與前述內容不符,則會配對。
範例:不包含前接 X
的小寫字母序列。
> 'Xabc def'.match(/(?<!X)[a-z]+/g)[ 'bc', 'def' ]
範例:將「.js」替換為「.html」,但「Node.js」除外。
> 'Node.js: index.js and main.js'.replace(/(?<!Node)\.js/g, '.html')'Node.js: index.html and main.html'
|
)注意事項:此運算子優先順序較低。如有必要,請使用群組。
^aa|zz$
會配對所有以 aa
開頭和/或以 zz
結尾的字串。請注意,|
的優先順序低於 ^
和 $
。^(aa|zz)$
會配對兩個字串「aa」和「zz」。^a(a|z)z$
會配對兩個字串「aaz」和「azz」。文字旗標 | 屬性名稱 | ES | 說明 |
---|---|---|---|
d |
hasIndices |
ES2022 | 開啟配對索引 |
g |
global |
ES3 | 多次配對 |
i |
ignoreCase |
ES3 | 不區分大小寫地配對 |
m |
multiline |
ES3 | ^ 和 $ 會逐行配對 |
s |
dotAll |
ES2018 | 點號會配對行終止符 |
u |
unicode |
ES6 | Unicode 模式(建議使用) |
y |
sticky |
ES6 | 配對之間沒有字元 |
JavaScript 中提供下列正規表示式旗標(表 21 提供簡潔的概觀)
/d
(.hasIndices
):某些與 RegExp 相關的方法會傳回描述正規表示式在輸入字串中配對位置的配對物件。如果此旗標開啟,每個配對物件都會包含配對索引,告訴我們每個群組擷取的起始和結束位置。更多資訊:§43.5.1「配對物件中的配對索引 [ES2022]」。
/g
(.global
) 會徹底改變下列方法的運作方式。
RegExp.prototype.test()
RegExp.prototype.exec()
String.prototype.match()
說明請參閱 §43.7「旗標 /g
和 /y
,以及屬性 .lastIndex
」。簡單來說,沒有 /g
時,這些方法只會考慮輸入字串中正則表達式的第一個比對。加上 /g
後,它們會考慮所有比對。
/i
(.ignoreCase
) 開啟不區分大小寫的比對
> /a/.test('A')false
> /a/i.test('A')true
/m
(.multiline
):如果此旗標開啟,^
會比對每一行的開頭,而 $
會比對每一行的結尾。如果關閉,^
會比對整個輸入字串的開頭,而 $
會比對整個輸入字串的結尾。
> 'a1\na2\na3'.match(/^a./gm)[ 'a1', 'a2', 'a3' ]
> 'a1\na2\na3'.match(/^a./g)[ 'a1' ]
/u
(.unicode
):此旗標會為正則表達式開啟 Unicode 模式。此模式說明請參閱 下一個小節。
/y
(.sticky
):此旗標主要與 /g
搭配使用才有意義。當兩者都開啟時,任何比對都必須緊接在先前的比對之後(也就是說,必須從正則表達式物件的索引 .lastIndex
開始)。因此,第一個比對必須在索引 0。
> 'a1a2 a3'.match(/a./gy)[ 'a1', 'a2' ]
> '_a1a2 a3'.match(/a./gy) // first match must be at index 0null
> 'a1a2 a3'.match(/a./g)[ 'a1', 'a2', 'a3' ]
> '_a1a2 a3'.match(/a./g)[ 'a1', 'a2', 'a3' ]
/y
的主要使用案例是將記號化(在分析期間)。關於此旗標的更多資訊:§43.7「旗標 /g
和 /y
,以及屬性 .lastIndex
」。
/s
(.dotAll
):預設情況下,點號不會比對換行符號。加上此旗標後,它會比對
> /./.test('\n')false
> /./s.test('\n')true
解決方法:如果不支援 /s
,我們可以使用 [^]
取代點號。
> /[^]/.test('\n')true
考慮以下正則表達式:/“([^”]+)”/udg
我們應該以什麼順序列出它的旗標?有兩個選項
/dgu
/u
最基本等):/ugd
由於 (2) 並不顯而易見,因此 (1) 是較好的選擇。JavaScript 也將其用於 RegExp 屬性 .flags
> /a/ismudgy.flags'dgimsuy'
/u
的 Unicode 模式旗標 /u
會為正則表達式開啟特殊的 Unicode 模式。此模式會啟用多項功能
在模式中,我們可以使用 Unicode 碼點跳脫字元,例如 \u{1F42A}
來指定字元。碼元跳脫字元,例如 \u03B1
,只有一個範圍為四個十六進位數字(對應於基本多文種平面)。
在模式中,我們可以使用 Unicode 屬性跳脫字元,例如 \p{White_Space}
。
現在禁止許多跳脫字元。例如:\a \- \:
模式字元總是與它們自己比對
> /pa-:/.test('pa-:')true
沒有 /u
,有些模式字元如果用反斜線跳脫,仍然會與自身相符
> /\p\a\-\:/.test('pa-:')true
使用 /u
\p
開始 Unicode 屬性跳脫。相符的原子單位是 Unicode 字元(代碼點),而非 JavaScript 字元(代碼單位)。
以下小節會更詳細地說明最後一項。它們使用以下 Unicode 字元說明什麼時候原子單位是 Unicode 字元,什麼時候是 JavaScript 字元
const codePoint = '🙂';
const codeUnits = '\uD83D\uDE42'; // UTF-16
.equal(codePoint, codeUnits); // same string! assert
我只是在 🙂
和 \uD83D\uDE42
之間切換,來說明 JavaScript 如何看待事物。兩者是等效的,可以在字串和正規表示式中互換使用。
使用 /u
,🙂
的兩個代碼單位會被視為一個單一字元
> /^[🙂]$/u.test('🙂')true
沒有 /u
,🙂
會被視為兩個字元
> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42')false
> /^[\uD83D\uDE42]$/.test('\uDE42')true
請注意,^
和 $
要求輸入字串只有一個字元。這就是第一個結果為 false
的原因。
.
) 會相符 Unicode 字元,而非 JavaScript 字元使用 /u
,點運算子會相符 Unicode 字元
> '🙂'.match(/./gu).length1
.match()
加上 /g
會傳回一個陣列,其中包含正規表示式的所有相符項。
沒有 /u
,點運算子會相符 JavaScript 字元
> '\uD83D\uDE80'.match(/./g).length2
使用 /u
,量詞會套用於整個前一個 Unicode 字元
> /^🙂{3}$/u.test('🙂🙂🙂')true
沒有 /u
,量詞只會套用於前一個 JavaScript 字元
> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80')true
值得注意
.lastIndex
是真正的實例屬性。所有其他屬性都是透過 getter 實作的。.lastIndex
是唯一可變的屬性。所有其他屬性都是唯讀的。如果我們要變更這些屬性,我們需要複製正規表示式(請參閱 §43.1.2「複製和非破壞性地修改正規表示式」 以取得詳細資訊)。每個正規表示式旗標都存在於一個屬性中,該屬性具有較長且更具描述性的名稱
> /a/i.ignoreCasetrue
> /a/.ignoreCasefalse
以下是旗標屬性的完整清單
.dotAll
(/s
).global
(/g
).hasIndices
(/d
).ignoreCase
(/i
).multiline
(/m
).sticky
(/y
).unicode
(/u
)每個正規表示式還具有下列屬性
.source
[ES3]:正規表示式模式
> /abc/ig.source'abc'
.flags
[ES6]:正規表示式的旗標
> /abc/ig.flags'gi'
.lastIndex
[ES3]:在旗標 /g
開啟時使用。有關詳細資訊,請參閱 §43.7「旗標 /g
和 /y
,以及屬性 .lastIndex
」。
數個與正規表示式相關的方法會傳回所謂的比對物件,以提供正規表示式與輸入字串比對位置的詳細資訊。這些方法為
RegExp.prototype.exec()
傳回 null
或單一比對物件。String.prototype.match()
傳回 null
或單一比對物件(如果旗標 /g
未設定)。String.prototype.matchAll()
傳回比對物件的 iterable(必須設定旗標 /g
;否則,會擲回例外狀況)。以下是一個範例
.deepEqual(
assert/(a+)b/d.exec('ab aaab'),
{0: 'ab',
1: 'a',
index: 0,
input: 'ab aaab',
groups: undefined,
indices: {
0: [0, 2],
1: [0, 1],
groups: undefined
,
}
}; )
.exec()
的結果是與下列屬性的第一個比對相符的比對物件
[0]
:正規表示式比對到的完整子字串[1]
:編號群組 1 的擷取(依此類推).index
:比對發生在哪裡?.input
:比對的字串.groups
:命名群組的擷取(請參閱 §43.6.4.2「命名擷取群組 [ES2018]」).indices
:擷取群組的索引範圍
/d
時才會建立。比對索引是比對物件的一項功能:如果我們透過正規表示式旗標 /d
(屬性 .hasIndices
)開啟它,它們會記錄群組擷取的開始和結束索引。
以下是我們存取編號群組擷取的方式
const matchObj = /(a+)(b+)/d.exec('aaaabb');
.equal(
assert1], 'aaaa'
matchObj[;
).equal(
assert2], 'bb'
matchObj[; )
由於正規表示式旗標 /d
,matchObj
也有屬性 .indices
,它會記錄每個編號群組在輸入字串中擷取的位置
.deepEqual(
assert.indices[1], [0, 4]
matchObj;
).deepEqual(
assert.indices[2], [4, 6]
matchObj; )
命名群組的擷取會像這樣存取
const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
.equal(
assert.groups.as, 'aaaa');
matchObj.equal(
assert.groups.bs, 'bb'); matchObj
它們的索引儲存在 matchObj.indices.groups
.deepEqual(
assert.indices.groups.as, [0, 4]);
matchObj.deepEqual(
assert.indices.groups.bs, [4, 6]); matchObj
比對索引的一個重要使用案例是指出語法錯誤的確切位置的剖析器。下列程式碼解決了一個相關問題:它指出引號內容的開始和結束位置(請參閱最後的示範)。
const reQuoted = /“([^”]+)”/dgu;
function pointToQuotedText(str) {
const startIndices = new Set();
const endIndices = new Set();
for (const match of str.matchAll(reQuoted)) {
const [start, end] = match.indices[1];
.add(start);
startIndices.add(end);
endIndices
}let result = '';
for (let index=0; index < str.length; index++) {
if (startIndices.has(index)) {
+= '[';
result else if (endIndices.has(index+1)) {
} += ']';
result else {
} += ' ';
result
}
}return result;
}
.equal(
assertpointToQuotedText(
'They said “hello” and “goodbye”.'),
' [ ] [ ] '
; )
預設情況下,正規表示式會比對字串中的任何位置
> /a/.test('__a__')true
我們可以使用斷言(如 ^
)或使用旗標 /y
來變更此設定
> /^a/.test('__a__')false
> /^a/.test('a__')true
regExp.test(str)
:是否有比對結果?[ES3]正規表示式方法 .test()
會傳回 true
,表示 regExp
比對 str
的結果
> /bc/.test('ABCD')false
> /bc/i.test('ABCD')true
> /\.mjs$/.test('main.mjs')true
使用 .test()
時,我們通常應避免使用 /g
旗標。如果使用,我們通常無法在每次呼叫方法時取得相同的結果
> const r = /a/g;
> r.test('aab')true
> r.test('aab')true
> r.test('aab')false
此結果是因為 /a/
在字串中有兩個比對結果。在找到所有結果後,.test()
會傳回 false
。
str.search(regExp)
:比對結果在哪個索引位置?[ES3]字串方法 .search()
會傳回 str
中 regExp
的第一個比對結果索引位置
> '_abc_'.search(/abc/)1
> 'main.mjs'.search(/\.mjs$/)4
regExp.exec(str)
:擷取群組 [ES3]不使用旗標 /g
時,.exec()
會傳回 比對物件,表示 regExp
在 str
中的第一個比對結果
.deepEqual(
assert/(a+)b/.exec('ab aab'),
{0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: undefined,
}; )
前一個範例包含單一編號群組。下列範例示範命名群組
.deepEqual(
assert/(?<as>a+)b/.exec('ab aab'),
{0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: { as: 'a' },
}; )
在 .exec()
的結果中,我們可以看到命名群組也是編號群組,其擷取結果會出現兩次
'1'
)。groups.as
)。 取得所有比對結果的更佳替代方案:
str.matchAll(regExp)
[ES2020]
自 ECMAScript 2020 起,JavaScript 提供另一種方法來取得所有比對結果:str.matchAll(regExp)
。此方法較容易使用,且有較少的注意事項。
如果我們要取得正規表示式的所有比對結果(不只第一個),我們需要啟用旗標 /g
。然後,我們可以多次呼叫 .exec()
,每次取得一個比對結果。在最後一個比對結果之後,.exec()
會傳回 null
。
> const regExp = /(a+)b/g;
> regExp.exec('ab aab'){ 0: 'ab', 1: 'a', index: 0, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab'){ 0: 'aab', 1: 'aa', index: 3, input: 'ab aab', groups: undefined }
> regExp.exec('ab aab')null
因此,我們可以迴圈處理所有比對結果,如下所示
const regExp = /(a+)b/g;
const str = 'ab aab';
let match;
// Check for null via truthiness
// Alternative: while ((match = regExp.exec(str)) !== null)
while (match = regExp.exec(str)) {
console.log(match[1]);
}// Output:
// 'a'
// 'aa'
與
/g
共用正規表示式時請小心!
與 /g
共用正規表示式有幾個缺點,會在 後續說明。
練習:透過
.exec()
萃取引號文字
exercises/regexps/extract_quoted_test.mjs
str.match(regExp)
:取得所有第 0 群組的擷取 [ES3]沒有 /g
的情況下,.match()
的運作方式類似於 .exec()
,會傳回一個單一的比對物件。
有 /g
的情況下,.match()
會傳回 str
中所有符合 regExp
的子字串
> 'ab aab'.match(/(a+)b/g)[ 'ab', 'aab' ]
如果沒有比對,.match()
會傳回 null
> 'xyz'.match(/(a+)b/g)null
我們可以使用 空值合併運算子 (??
) 來避免 null
const numberOfMatches = (str.match(regExp) ?? []).length;
str.matchAll(regExp)
:取得所有比對物件的可迭代物件 [ES2020]以下是如何呼叫 .matchAll()
const matchIterable = str.matchAll(regExp);
給定一個字串和一個正規表示式,.matchAll()
會傳回一個可迭代物件,包含所有比對的比對物件。
在以下範例中,我們使用 Array.from()
將可迭代物件轉換為陣列,以便我們能更方便地比較它們。
> Array.from('-a-a-a'.matchAll(/-(a)/ug))[
{ 0:'-a', 1:'a', index: 0, input: '-a-a-a', groups: undefined },
{ 0:'-a', 1:'a', index: 2, input: '-a-a-a', groups: undefined },
{ 0:'-a', 1:'a', index: 4, input: '-a-a-a', groups: undefined },
]
必須設定旗標 /g
> Array.from('-a-a-a'.matchAll(/-(a)/u))TypeError: String.prototype.matchAll called with a non-global
RegExp argument
.matchAll()
不受 regExp.lastIndex
影響,也不會變更它。
.matchAll()
.matchAll()
可以透過 .exec()
如下實作
function* matchAll(str, regExp) {
if (!regExp.global) {
throw new TypeError('Flag /g must be set!');
}const localCopy = new RegExp(regExp, regExp.flags);
let match;
while (match = localCopy.exec(str)) {
yield match;
} }
建立一個本機副本可確保兩件事
regex.lastIndex
沒有變更。localCopy.lastIndex
為零。使用 matchAll()
const str = '"fee" "fi" "fo" "fum"';
const regex = /"([^"]*)"/g;
for (const match of matchAll(str, regex)) {
console.log(match[1]);
}// Output:
// 'fee'
// 'fi'
// 'fo'
// 'fum'
regExp.exec()
vs. str.match()
vs. str.matchAll()
下表總結了這三種方法的差異
沒有 /g |
有 /g |
|
---|---|---|
regExp.exec(str) |
第一個比對物件 | 下一個比對物件或 null |
str.match(regExp) |
第一個比對物件 | 第 0 群組擷取的陣列 |
str.matchAll(regExp) |
TypeError |
比對物件的可迭代物件 |
str.replace()
和 str.replaceAll()
替換兩種替換方法都有兩個參數
str.replace(searchValue, replacementValue)
str.replaceAll(searchValue, replacementValue)
searchValue
可以是
replacementValue
可以是
$
有特殊意義,讓我們能插入群組的擷取等(稍後會說明詳細內容)。這兩種方法的差異如下
.replace()
替換沒有 /g
的字串或正規表示式的第一次出現。.replaceAll()
替換有 /g
的字串或正規表示式的所有出現。此表格總結其運作方式
搜尋:→ |
字串 | 不含 /g 的正規表示法 |
含 /g 的正規表示法 |
---|---|---|---|
.replace |
第一次出現 | 第一次出現 | (所有出現) |
.replaceAll |
所有出現 | TypeError |
所有出現 |
.replace()
的最後一欄以括號標示,這是因為此方法早於 .replaceAll()
存在,因此支援現在應透過後者方法處理的功能。如果我們可以變更,.replace()
會在此拋出 TypeError
。
我們先探討當 replacementValue
是簡單字串(不含字元 $
)時,.replace()
和 .replaceAll()
如何個別運作。然後我們檢查更複雜的替換值如何影響兩者。
str.replace(searchValue, replacementValue)
[ES3].replace()
的運作方式受其第一個參數 searchValue
影響
不含 /g
的正規表示法:取代此正規表示法的第一次匹配。
> 'aaa'.replace(/a/, 'x')'xaa'
字串:取代此字串的第一次出現(字串會逐字解釋,而不是正規表示法)。
> 'aaa'.replace('a', 'x')'xaa'
含 /g
的正規表示法:取代此正規表示法的全部匹配。
> 'aaa'.replace(/a/g, 'x')'xxx'
建議:如果 .replaceAll()
可用,最好在此情況下使用該方法,其目的就是取代多個出現。
如果我們想取代字串的每個出現,我們有兩個選項
我們可以使用 .replaceAll()
(ES2021 中引入)。
在本章節後續,我們會遇到 [工具函式 escapeForRegExp()
),它將協助我們將字串轉換為正規表示法,以多次匹配該字串(例如,'*'
會變成 /\*/g
)。
str.replaceAll(searchValue, replacementValue)
[ES2021].replaceAll()
的運作方式受其第一個參數 searchValue
影響
含 /g
的正規表示法:取代此正規表示法的全部匹配。
> 'aaa'.replaceAll(/a/g, 'x')'xxx'
字串:取代此字串的全部出現(字串會逐字解釋,而不是正規表示法)。
> 'aaa'.replaceAll('a', 'x')'xxx'
不含 /g
的正規表示法:會拋出 TypeError
(因為 .replaceAll()
的目的是取代多個出現)。
> 'aaa'.replaceAll(/a/, 'x')TypeError: String.prototype.replaceAll called with
a non-global RegExp argument
.replace()
和 .replaceAll()
的參數 replacementValue
到目前為止,我們只使用參數 replacementValue
搭配簡單字串,但它可以做更多事。如果其值是
字串,則匹配會以這個字串取代。字元 $
有特殊意義,讓我們插入群組擷取等(繼續閱讀以取得詳細資料)。
函式,則匹配會以透過此函式計算的字串取代。
replacementValue
是一個字串如果替換值是一個字串,則美元符號有特殊意義 – 它會插入與正規表示式相符的文字
文字 | 結果 |
---|---|
$$ |
單一 $ |
$& |
完整相符 |
$` |
相符前的文字 |
$' |
相符後的文字 |
$n |
編號群組 n 的擷取 (n > 0) |
$<name> |
命名群組 name 的擷取 [ES2018] |
範例:插入相符子字串之前、之中和之後的文字。
> 'a1 a2'.replaceAll(/a/g, "($`|$&|$')")'(|a|1 a2)1 (a1 |a|2)2'
範例:插入編號群組的擷取。
> const regExp = /^([A-Za-z]+): (.*)$/ug;
> 'first: Jane'.replaceAll(regExp, 'KEY: $1, VALUE: $2')'KEY: first, VALUE: Jane'
範例:插入命名群組的擷取。
> const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/ug;
> 'first: Jane'.replaceAll(regExp, 'KEY: $<key>, VALUE: $<value>')'KEY: first, VALUE: Jane'
練習:透過
.replace()
和命名群組變更引號
exercises/regexps/change_quotes_test.mjs
replacementValue
是一個函式如果替換值是一個函式,我們可以計算每個替換。在以下範例中,我們將找到的每個非負整數乘以二。
.equal(
assert'3 cats and 4 dogs'.replaceAll(/[0-9]+/g, (all) => 2 * Number(all)),
'6 cats and 8 dogs'
; )
替換函式取得以下參數。請注意它們與相符物件有多麼相似。這些參數都是位置參數,但我已包含可以如何命名它們
all
:完整相符g1
:編號群組 1 的擷取index
:相符發生在哪裡?input
:我們正在替換的字串groups
[ES2018]:命名群組的擷取(一個物件)。總是最後一個參數。如果我們只對 groups
有興趣,則可以使用下列技巧
const result = 'first=jane, last=doe'.replace(
/(?<key>[a-z]+)=(?<value>[a-z]+)/g,
...args) => { // (A)
(const groups = args.at(-1); // (B)
const {key, value} = groups;
return key.toUpperCase() + '=' + value.toUpperCase();
;
}).equal(result, 'FIRST=JANE, LAST=DOE'); assert
由於第 A 行中的 剩餘參數,args
包含一個包含所有參數的陣列。我們透過第 B 行中的 陣列方法 .at()
存取最後一個參數。
String.prototype.split()
在字串章節中說明。String.prototype.split()
的第一個參數是一個字串或一個正規表示式。如果後者,則群組的擷取會出現在結果中
> 'a:b : c'.split(':')[ 'a', 'b ', ' c' ]
> 'a:b : c'.split(/ *: */)[ 'a', 'b', 'c' ]
> 'a:b : c'.split(/( *):( *)/)[ 'a', '', '', 'b', ' ', ' ', 'c' ]
/g
和 /y
,以及屬性 .lastIndex
(進階)在本節中,我們將探討 RegExp 旗標 /g
和 /y
如何運作,以及它們如何依賴 RegExp 屬性 .lastIndex
。我們還會發現 .lastIndex
一個有趣的用例,你可能會感到驚訝。
/g
和 /y
每個方法對 /g
和 /y
的反應都不同;這讓我們對一般情況有粗略的了解
/g
(.global
,ES3):正規表示式應該在字串中任意位置多次比對。/y
(.sticky
,ES6):字串中的任何比對都應該緊接在先前的比對之後(比對會「黏」在一起)。如果正規表示式既沒有旗標 /g
也不沒有旗標 /y
,比對會發生一次並從開頭開始。
使用 /g
或 /y
時,比對會相對於輸入字串中的「目前位置」執行。該位置儲存在正規表示式屬性 .lastIndex
中。
有 3 組與正規表示式相關的方法
字串方法 .search(regExp)
和 .split(regExp)
完全忽略 /g
和 /y
(因此也忽略 .lastIndex
)。
如果設定 /g
或 /y
,RegExp
方法 .exec(str)
和 .test(str)
會以兩種方式變更。
首先,我們可以重複呼叫一個方法來取得多個比對。每次呼叫都會傳回另一個結果(比對物件或 true
)或「結果結束」值(null
或 false
)。
其次,正規表示式屬性 .lastIndex
用於逐步處理輸入字串。一方面,.lastIndex
決定比對從何處開始
/g
表示比對必須從 .lastIndex
或之後開始。
/y
表示比對必須從 .lastIndex
開始。也就是說,正規表示式的開頭會固定在 .lastIndex
。
請注意,^
和 $
仍會像平常一樣運作:它們會將比對固定在輸入字串的開頭或結尾,除非設定 .multiline
。然後它們會固定在行的開頭或結尾。
另一方面,.lastIndex
會設定為前一個比對的最後一個索引值加一。
所有其他方法會受到以下影響
/g
會產生多個比對。/y
會產生一個單一比對,且必須從 .lastIndex
開始。/yg
會產生沒有間隙的多個比對。這是第一個概觀。後面的章節會深入探討更多細節。
/g
和 /y
影響?regExp.exec(str)
[ES3]沒有 /g
和 /y
時,.exec()
會忽略 .lastIndex
,並總是傳回第一個比對的比對物件
> const re = /#/; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex][{ 0: '#', index: 0, input: '##-#' }, 1]
> [re.exec('##-#'), re.lastIndex][{ 0: '#', index: 0, input: '##-#' }, 1]
使用 /g
時,比對必須從 .lastIndex
或之後開始。.lastIndex
會更新。如果沒有比對,則傳回 null
。
> const re = /#/g; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex][{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex][{ 0: '#', index: 3, input: '##-#' }, 4]
> [re.exec('##-#'), re.lastIndex][null, 0]
使用 /y
時,比對必須從 .lastIndex
的位置開始。.lastIndex
會更新。如果沒有比對到,會傳回 null
。
> const re = /#/y; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex][{ 0: '#', index: 1, input: '##-#' }, 2]
> [re.exec('##-#'), re.lastIndex][null, 0]
使用 /yg
時,.exec()
的行為與使用 /y
相同。
regExp.test(str)
[ES3]這個方法的行為與 .exec()
相同,但傳回 true
而不是傳回比對物件,傳回 false
而不是傳回 null
。
例如,不使用 /g
或 /y
時,結果總是 true
> const re = /#/; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex][true, 1]
> [re.test('##-#'), re.lastIndex][true, 1]
使用 /g
時,會有兩個比對
> const re = /#/g; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex][true, 2]
> [re.test('##-#'), re.lastIndex][true, 4]
> [re.test('##-#'), re.lastIndex][false, 0]
使用 /y
時,只有一個比對
> const re = /#/y; re.lastIndex = 1;
> [re.test('##-#'), re.lastIndex][true, 2]
> [re.test('##-#'), re.lastIndex][false, 0]
使用 /yg
時,.test()
的行為與使用 /y
相同。
str.match(regExp)
[ES3]不使用 /g
時,.match()
的作用與 .exec()
相同。不使用 /y
> const re = /#/; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex][{ 0: '#', index: 0, input: '##-#' }, 1]
> ['##-#'.match(re), re.lastIndex][{ 0: '#', index: 0, input: '##-#' }, 1]
或使用 /y
> const re = /#/y; re.lastIndex = 1;
> ['##-#'.match(re), re.lastIndex][{ 0: '#', index: 1, input: '##-#' }, 2]
> ['##-#'.match(re), re.lastIndex][null, 0]
使用 /g
時,我們會在陣列中取得所有比對(第 0 群組)。.lastIndex
會被忽略並重設為零。
> const re = /#/g; re.lastIndex = 1;
> '##-#'.match(re)['#', '#', '#']
> re.lastIndex0
/yg
的作用與 /g
相同,但比對之間沒有間隙
> const re = /#/yg; re.lastIndex = 1;
> '##-#'.match(re)['#', '#']
> re.lastIndex0
str.matchAll(regExp)
[ES2020]如果沒有設定 /g
,.matchAll()
會擲回例外
> const re = /#/y; re.lastIndex = 1;
> '##-#'.matchAll(re)TypeError: String.prototype.matchAll called with
a non-global RegExp argument
如果設定了 /g
,比對會從 .lastIndex
開始,且該屬性不會變更
> const re = /#/g; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))[
{ 0: '#', index: 1, input: '##-#' },
{ 0: '#', index: 3, input: '##-#' },
]
> re.lastIndex1
如果設定了 /yg
,行為與使用 /g
相同,但比對之間沒有間隙
> const re = /#/yg; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))[
{ 0: '#', index: 1, input: '##-#' },
]
> re.lastIndex1
str.replace(regExp, str)
[ES3]不使用 /g
和 /y
時,只會取代第一次出現的字串
> const re = /#/; re.lastIndex = 1;
> '##-#'.replace(re, 'x')'x#-#'
> re.lastIndex1
使用 /g
時,會取代所有出現的字串。.lastIndex
會被忽略,但會重設為零。
> const re = /#/g; re.lastIndex = 1;
> '##-#'.replace(re, 'x')'xx-x'
> re.lastIndex0
使用 /y
時,只會取代 .lastIndex
處的(第一次)出現的字串。.lastIndex
會更新。
> const re = /#/y; re.lastIndex = 1;
> '##-#'.replace(re, 'x')'#x-#'
> re.lastIndex2
/yg
的作用與 /g
相同,但比對之間不允許有間隙
> const re = /#/yg; re.lastIndex = 1;
> '##-#'.replace(re, 'x')'xx-#'
> re.lastIndex0
str.replaceAll(regExp, str)
[ES2021].replaceAll()
的作用與 .replace()
相同,但如果沒有設定 /g
,會擲回例外
> const re = /#/y; re.lastIndex = 1;
> '##-#'.replaceAll(re, 'x')TypeError: String.prototype.replaceAll called
with a non-global RegExp argument
/g
和 /y
的四個陷阱以及如何處理我們將先探討 /g
和 /y
的四個陷阱,然後探討如何處理這些陷阱。
/g
或 /y
中無法將正規表示式內嵌在 /g
中。例如,在以下 while
迴圈中,每次檢查條件時都會重新建立正規表示式。因此,它的 .lastIndex
永遠都是零,迴圈永遠不會終止。
let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
console.log(matchObj[0]);
}
使用 /y
時,問題相同。
/g
或 /y
可能會損壞程式碼如果程式碼預期一個帶有 /g
的正規表示式,並且對 .exec()
或 .test()
的結果進行迴圈,那麼沒有 /g
的正規表示式可能會導致無限迴圈
function collectMatches(regExp, str) {
const matches = [];
let matchObj;
// Infinite loop
while (matchObj = regExp.exec(str)) {
.push(matchObj[0]);
matches
}return matches;
}collectMatches(/a+/, 'bbbaabaaa'); // Missing: flag /g
為什麼會發生無限迴圈?因為 .exec()
永遠會傳回第一個結果,一個匹配物件,而不會是 null
。
使用 /y
時,問題相同。
/g
或 /y
可能會損壞程式碼對於 .test()
,還有另一個需要注意的地方:它會受到 .lastIndex
的影響。因此,如果我們想要精確地檢查一次正規表示式是否與字串相符,那麼正規表示式不能有 /g
。否則,我們每次呼叫 .test()
時通常會得到不同的結果
> const regExp = /^X/g;
> [regExp.test('Xa'), regExp.lastIndex][ true, 1 ]
> [regExp.test('Xa'), regExp.lastIndex][ false, 0 ]
> [regExp.test('Xa'), regExp.lastIndex][ true, 1 ]
第一次呼叫會產生一個匹配並更新 .lastIndex
。第二次呼叫不會找到匹配並將 .lastIndex
重設為零。
如果我們特別為 .test()
建立一個正規表示式,那麼我們可能不會新增 /g
。但是,如果我們對替換和測試使用同一個正規表示式,那麼遇到 /g
的可能性就會增加。
這個問題同樣也存在於 /y
> const regExp = /^X/y;
> regExp.test('Xa')true
> regExp.test('Xa')false
> regExp.test('Xa')true
.lastIndex
不是零,程式碼可能會產生意外的結果由於所有會受到 .lastIndex
影響的正規表示式運算,我們必須小心許多演算法,在開始時 .lastIndex
為零。否則,我們可能會得到意外的結果
function countMatches(regExp, str) {
let count = 0;
while (regExp.test(str)) {
++;
count
}return count;
}
const myRegExp = /a/g;
.lastIndex = 4;
myRegExp.equal(
assertcountMatches(myRegExp, 'babaa'), 1); // should be 3
通常,在新建的正規表示式中 .lastIndex
為零,而且我們不會像範例中那樣明確地改變它。但是,如果我們多次使用正規表示式,.lastIndex
仍然可能不會為零。
/g
和 /y
的陷阱作為處理 /g
和 .lastIndex
的範例,我們重新檢視前一個範例中的 countMatches()
。我們如何防止錯誤的正規表示式損壞我們的程式碼?讓我們來看三種方法。
首先,如果沒有設定 /g
或 .lastIndex
不是零,我們可以拋出例外
function countMatches(regExp, str) {
if (!regExp.global) {
throw new Error('Flag /g of regExp must be set');
}if (regExp.lastIndex !== 0) {
throw new Error('regExp.lastIndex must be zero');
}
let count = 0;
while (regExp.test(str)) {
++;
count
}return count;
}
其次,我們可以複製參數。這樣的好處是 regExp
就不會被改變。
function countMatches(regExp, str) {
const cloneFlags = regExp.flags + (regExp.global ? '' : 'g');
const clone = new RegExp(regExp, cloneFlags);
let count = 0;
while (clone.test(str)) {
++;
count
}return count;
}
.lastIndex
或旗標影響的運算數個正規表示式運算不受 .lastIndex
或旗標影響。例如,如果存在 /g
,.match()
會忽略 .lastIndex
function countMatches(regExp, str) {
if (!regExp.global) {
throw new Error('Flag /g of regExp must be set');
}return (str.match(regExp) ?? []).length;
}
const myRegExp = /a/g;
.lastIndex = 4;
myRegExp.equal(countMatches(myRegExp, 'babaa'), 3); // OK! assert
在此,即使我們沒有檢查或修正 .lastIndex
,countMatches()
仍會運作。
.lastIndex
的使用案例:從給定的索引開始比對除了儲存狀態,.lastIndex
也可用於從給定的索引開始比對。本節說明如何執行。
由於 .test()
受 /y
和 .lastIndex
影響,我們可以使用它來檢查正規表示式 regExp
是否在給定的 index
比對字串 str
function matchesStringAt(regExp, str, index) {
if (!regExp.sticky) {
throw new Error('Flag /y of regExp must be set');
}.lastIndex = index;
regExpreturn regExp.test(str);
}.equal(
assertmatchesStringAt(/x+/y, 'aaxxx', 0), false);
.equal(
assertmatchesStringAt(/x+/y, 'aaxxx', 2), true);
由於 /y
,regExp
會錨定至 .lastIndex
。
請注意,我們不得使用會將 regExp
錨定至輸入字串開頭的斷言 ^
。
.search()
讓我們找出正規表示式比對的位置
> '#--#'.search(/#/)0
唉,我們無法變更 .search()
開始尋找比對的位置。作為解決方法,我們可以使用 .exec()
進行搜尋
function searchAt(regExp, str, index) {
if (!regExp.global && !regExp.sticky) {
throw new Error('Either flag /g or flag /y of regExp must be set');
}.lastIndex = index;
regExpconst match = regExp.exec(str);
if (match) {
return match.index;
else {
} return -1;
}
}
.equal(
assertsearchAt(/#/g, '#--#', 0), 0);
.equal(
assertsearchAt(/#/g, '#--#', 1), 3);
當與 /y
一起使用且不使用 /g
時,.replace()
會進行一次取代,如果 .lastIndex
有比對
function replaceOnceAt(str, regExp, replacement, index) {
if (!(regExp.sticky && !regExp.global)) {
throw new Error('Flag /y must be set, flag /g must not be set');
}.lastIndex = index;
regExpreturn str.replace(regExp, replacement);
}.equal(
assertreplaceOnceAt('aa aaaa a', /a+/y, 'X', 0), 'X aaaa a');
.equal(
assertreplaceOnceAt('aa aaaa a', /a+/y, 'X', 3), 'aa X a');
.equal(
assertreplaceOnceAt('aa aaaa a', /a+/y, 'X', 8), 'aa aaaa X');
.lastIndex
的缺點正規表示式屬性 .lastIndex
有兩個重大的缺點
.lastIndex
的支援在正規表示式運算之間不一致。好處是,.lastIndex
也為我們提供了其他有用的功能:我們可以指定比對應從何處開始(對於某些運算)。
.global
(/g
) 和 .sticky
(/y
)下列兩個方法完全不受 /g
和 /y
影響
String.prototype.search()
String.prototype.split()
此表格說明其餘正規表示式相關方法如何受這兩個旗標影響
/ |
/g |
/y |
/yg |
|
---|---|---|---|---|
r.exec(s) |
{i:0} |
{i:1} |
{i:1} |
{i:1} |
.lI unch |
.lI upd |
.lI upd |
.lI upd |
|
r.test(s) |
true |
true |
true |
true |
.lI unch |
.lI upd |
.lI upd |
.lI upd |
|
s.match(r) |
{i:0} |
["#","#","#"] |
{i:1} |
["#","#"] |
.lI unch |
.lI reset |
.lI upd |
.lI reset |
|
s.matchAll(r) |
TypeError |
[{i:1}, {i:3}] |
TypeError |
[{i:1}] |
.lI unch |
.lI unch |
|||
s.replace(r, 'x') |
"x#-#" |
"xx-x" |
"#x-#" |
"xx-#" |
.lI unch |
.lI reset |
.lI upd |
.lI reset |
|
s.replaceAll(r, 'x') |
TypeError |
"xx-x" |
TypeError |
"xx-#" |
.lI reset |
.lI reset |
變數
const r = /#/; r.lastIndex = 1;
const s = '##-#';
縮寫
{i:2}
:比對物件,其屬性 .index
的值為 2
.lI
upd:更新 .lastIndex
.lI
reset:將 .lastIndex
重設為零.lI
unch:不變更 .lastIndex
產生前述表格的 Node.js 程式碼
前述表格是透過 Node.js 程式碼 產生的。
下列函式會轉譯任意文字,以便在正規表示式中使用時,能逐字比對
function escapeForRegExp(str) {
return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); // (A)
}.equal(escapeForRegExp('[yes?]'), String.raw`\[yes\?\]`);
assert.equal(escapeForRegExp('_g_'), String.raw`_g_`); assert
在 A 行,我們轉譯所有語法字元。我們必須有選擇性,因為正規表示式旗標 /u
禁止許多轉譯字元,例如:\a \: \-
escapeForRegExp()
有兩個使用案例
new RegExp()
動態建立的正規表示式中。.replace()
取代純文字字串的所有出現(且無法使用 .replaceAll()
)。.replace()
僅允許我們取代純文字一次。透過 escapeForRegExp()
,我們可以解決此限制
const plainText = ':-)';
const regExp = new RegExp(escapeForRegExp(plainText), 'ug');
.equal(
assert':-) :-) :-)'.replace(regExp, '🙂'), '🙂 🙂 🙂');
有時,我們可能需要一個比對所有或無任何內容的正規表示式,例如,作為預設值。
比對所有內容:/(?:)/
空群組 ()
比對所有內容。我們使其為非擷取式(透過 ?:
),以避免不必要的運算。
> /(?:)/.test('')true
> /(?:)/.test('abc')true
比對無任何內容:/.^/
^
僅比對字串開頭。句點將比對移至第一個字元之後,而現在 ^
不再比對。
> /.^/.test('')false
> /.^/.test('abc')false