せっかちなプログラマーのための JavaScript (ES2022 版)
この本をサポートしてください: 購入 または 寄付
(広告、ブロックしないでください)

43 正規表現 (RegExp)



  功能可用性

除非另有說明,否則每個正規表示式功能自 ES3 起即可使用。

43.1 建立正規表示式

43.1.1 文字與建構函式

建立正規表示式的兩種主要方法為

兩種正規表示式都有相同的兩個部分

43.1.2 複製和非破壞性修改正規表示式

RegExp() 建構函式有兩個變體

第二個變體適用於複製正規表示式,同時可以選擇修改它們。旗標是不可變的,這是變更它們的唯一方法,例如

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);
}
assert.equal(/abc/i.flags, 'i');
assert.equal(copyAndAddFlags(/abc/i, 'g').flags, 'gi');

43.2 語法

43.2.1 語法字元

在正規表示式的頂層,下列語法字元是特殊字元。它們會透過加上反斜線 (\) 作為前綴來跳脫。

\ ^ $ . * + ? ( ) [ ] { } |

在正規表示式字串中,我們必須跳脫斜線

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

new RegExp() 的引數中,我們不必跳脫斜線

> new RegExp('/').test('/')
true

43.2.2 基本原子

原子是正規表示式的基本組成元素。

43.2.3 Unicode 屬性跳脫 [ES2018]

43.2.3.1 Unicode 字元屬性

在 Unicode 標準中,每個字元都有屬性,也就是描述它的元資料。屬性在定義字元的性質中扮演重要的角色。引用Unicode 標準,第 3.3 節,D3

字元的語義由其身分、規範屬性和行為決定。

以下列出一些屬性的範例

43.2.3.2 Unicode 屬性跳脫

Unicode 屬性跳脫看起來像這樣

  1. \p{prop=value}:符合其屬性 prop 的值為 value 的所有字元。
  2. \P{prop=value}:符合其屬性 prop 的值不為 value 的所有字元。
  3. \p{bin_prop}:符合其二進制屬性 bin_prop 為 True 的所有字元。
  4. \P{bin_prop}:符合其二進制屬性 bin_prop 為 False 的所有字元。

註解

範例

進一步閱讀

43.2.4 字元類別

字元類別以方括號包住類別範圍。類別範圍指定一組字元

類別範圍的規則

43.2.5 群組

43.2.6 量詞

預設情況下,所有下列量詞都是貪婪的(它們會符合盡可能多的字元)

若要讓它們變成不貪婪的(讓它們符合盡可能少的字元),請在它們後面加上問號 (?)

> /".*"/.exec('"abc"def"')[0]  // greedy
'"abc"def"'
> /".*?"/.exec('"abc"def"')[0] // reluctant
'"abc"'

43.2.7 斷言

43.2.7.1 前瞻斷言

正前瞻: (?=«樣式») 如果 樣式 符合接下來的內容,則符合。

範例:由小寫字母組成的序列,其後接一個 X

> 'abcX def'.match(/[a-z]+(?=X)/g)
[ 'abc' ]

請注意,X 本身並非符合子字串的一部分。

負向先行斷言: (?!«pattern») 如果 pattern 與後續內容不符,則會配對。

範例:不包含後接 X 的小寫字母序列。

> 'abcX def'.match(/[a-z]+(?!X)/g)
[ 'ab', 'def' ]
43.2.7.2 後向斷言 [ES2018]

正向後向斷言: (?<=«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'

43.2.8 選項 (|)

注意事項:此運算子優先順序較低。如有必要,請使用群組。

43.3 旗標

表 21:以下是 JavaScript 支援的正規表示式旗標。
文字旗標 屬性名稱 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 提供簡潔的概觀)

43.3.1 如何排列正則表達式旗標?

考慮以下正則表達式:/“([^”]+)”/udg

我們應該以什麼順序列出它的旗標?有兩個選項

  1. 字母順序:/dgu
  2. 依重要性順序(可以說,/u 最基本等):/ugd

由於 (2) 並不顯而易見,因此 (1) 是較好的選擇。JavaScript 也將其用於 RegExp 屬性 .flags

> /a/ismudgy.flags
'dgimsuy'

43.3.2 旗標:透過 /u 的 Unicode 模式

旗標 /u 會為正則表達式開啟特殊的 Unicode 模式。此模式會啟用多項功能

以下小節會更詳細地說明最後一項。它們使用以下 Unicode 字元說明什麼時候原子單位是 Unicode 字元,什麼時候是 JavaScript 字元

const codePoint = '🙂';
const codeUnits = '\uD83D\uDE42'; // UTF-16

assert.equal(codePoint, codeUnits); // same string!

我只是在 🙂\uD83D\uDE42 之間切換,來說明 JavaScript 如何看待事物。兩者是等效的,可以在字串和正規表示式中互換使用。

43.3.2.1 結果:我們可以在字元類別中放置 Unicode 字元

使用 /u🙂 的兩個代碼單位會被視為一個單一字元

> /^[🙂]$/u.test('🙂')
true

沒有 /u🙂 會被視為兩個字元

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

請注意,^$ 要求輸入字串只有一個字元。這就是第一個結果為 false 的原因。

43.3.2.2 結果:點運算子 (.) 會相符 Unicode 字元,而非 JavaScript 字元

使用 /u,點運算子會相符 Unicode 字元

> '🙂'.match(/./gu).length
1

.match() 加上 /g 會傳回一個陣列,其中包含正規表示式的所有相符項。

沒有 /u,點運算子會相符 JavaScript 字元

> '\uD83D\uDE80'.match(/./g).length
2
43.3.2.3 結果:量詞套用於 Unicode 字元,而非 JavaScript 字元

使用 /u,量詞會套用於整個前一個 Unicode 字元

> /^🙂{3}$/u.test('🙂🙂🙂')
true

沒有 /u,量詞只會套用於前一個 JavaScript 字元

> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80')
true

43.4 正規表示式物件的屬性

值得注意

43.4.1 旗標作為屬性

每個正規表示式旗標都存在於一個屬性中,該屬性具有較長且更具描述性的名稱

> /a/i.ignoreCase
true
> /a/.ignoreCase
false

以下是旗標屬性的完整清單

43.4.2 其他屬性

每個正規表示式還具有下列屬性

43.5 比對物件

數個與正規表示式相關的方法會傳回所謂的比對物件,以提供正規表示式與輸入字串比對位置的詳細資訊。這些方法為

以下是一個範例

assert.deepEqual(
  /(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() 的結果是與下列屬性的第一個比對相符的比對物件

43.5.1 比對物件中的比對索引 [ES2022]

比對索引是比對物件的一項功能:如果我們透過正規表示式旗標 /d(屬性 .hasIndices)開啟它,它們會記錄群組擷取的開始和結束索引。

43.5.1.1 編號群組的比對索引

以下是我們存取編號群組擷取的方式

const matchObj = /(a+)(b+)/d.exec('aaaabb');
assert.equal(
  matchObj[1], 'aaaa'
);
assert.equal(
  matchObj[2], 'bb'
);

由於正規表示式旗標 /dmatchObj 也有屬性 .indices,它會記錄每個編號群組在輸入字串中擷取的位置

assert.deepEqual(
  matchObj.indices[1], [0, 4]
);
assert.deepEqual(
  matchObj.indices[2], [4, 6]
);
43.5.1.2 命名群組的比對索引

命名群組的擷取會像這樣存取

const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');
assert.equal(
  matchObj.groups.as, 'aaaa');
assert.equal(
  matchObj.groups.bs, 'bb');

它們的索引儲存在 matchObj.indices.groups

assert.deepEqual(
  matchObj.indices.groups.as, [0, 4]);
assert.deepEqual(
  matchObj.indices.groups.bs, [4, 6]);
43.5.1.3 更實際的範例

比對索引的一個重要使用案例是指出語法錯誤的確切位置的剖析器。下列程式碼解決了一個相關問題:它指出引號內容的開始和結束位置(請參閱最後的示範)。

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];
    startIndices.add(start);
    endIndices.add(end);
  }
  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;
}

assert.equal(
  pointToQuotedText(
    'They said “hello” and “goodbye”.'),
    '           [   ]       [     ]  '
);

43.6 處理正規表示式的方法

43.6.1 預設情況下,正規表示式會比對字串中的任何位置

預設情況下,正規表示式會比對字串中的任何位置

> /a/.test('__a__')
true

我們可以使用斷言(如 ^)或使用旗標 /y 來變更此設定

> /^a/.test('__a__')
false
> /^a/.test('a__')
true

43.6.2 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

43.6.3 str.search(regExp):比對結果在哪個索引位置?[ES3]

字串方法 .search() 會傳回 strregExp 的第一個比對結果索引位置

> '_abc_'.search(/abc/)
1
> 'main.mjs'.search(/\.mjs$/)
4

43.6.4 regExp.exec(str):擷取群組 [ES3]

43.6.4.1 取得第一個比對結果的比對物件

不使用旗標 /g 時,.exec() 會傳回 比對物件,表示 regExpstr 中的第一個比對結果

assert.deepEqual(
  /(a+)b/.exec('ab aab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: undefined,
  }
);
43.6.4.2 命名擷取群組 [ES2018]

前一個範例包含單一編號群組。下列範例示範命名群組

assert.deepEqual(
  /(?<as>a+)b/.exec('ab aab'),
  {
    0: 'ab',
    1: 'a',
    index: 0,
    input: 'ab aab',
    groups: { as: 'a' },
  }
);

.exec() 的結果中,我們可以看到命名群組也是編號群組,其擷取結果會出現兩次

43.6.4.3 迴圈處理所有比對結果

  取得所有比對結果的更佳替代方案: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

43.6.5 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;

43.6.6 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 影響,也不會變更它。

43.6.6.1 實作 .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;
  }
}

建立一個本機副本可確保兩件事

使用 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'

43.6.7 regExp.exec() vs. str.match() vs. str.matchAll()

下表總結了這三種方法的差異

沒有 /g /g
regExp.exec(str) 第一個比對物件 下一個比對物件或 null
str.match(regExp) 第一個比對物件 第 0 群組擷取的陣列
str.matchAll(regExp) TypeError 比對物件的可迭代物件

43.6.8 使用 str.replace()str.replaceAll() 替換

兩種替換方法都有兩個參數

searchValue 可以是

replacementValue 可以是

這兩種方法的差異如下

此表格總結其運作方式

搜尋: 字串 不含 /g 的正規表示法 /g 的正規表示法
.replace 第一次出現 第一次出現 (所有出現)
.replaceAll 所有出現 TypeError 所有出現

.replace() 的最後一欄以括號標示,這是因為此方法早於 .replaceAll() 存在,因此支援現在應透過後者方法處理的功能。如果我們可以變更,.replace() 會在此拋出 TypeError

我們先探討當 replacementValue 是簡單字串(不含字元 $)時,.replace().replaceAll() 如何個別運作。然後我們檢查更複雜的替換值如何影響兩者。

43.6.8.1 str.replace(searchValue, replacementValue) [ES3]

.replace() 的運作方式受其第一個參數 searchValue 影響

如果我們想取代字串的每個出現,我們有兩個選項

43.6.8.2 str.replaceAll(searchValue, replacementValue) [ES2021]

.replaceAll() 的運作方式受其第一個參數 searchValue 影響

43.6.8.3 .replace().replaceAll() 的參數 replacementValue

到目前為止,我們只使用參數 replacementValue 搭配簡單字串,但它可以做更多事。如果其值是

43.6.8.4 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

43.6.8.5 replacementValue 是一個函式

如果替換值是一個函式,我們可以計算每個替換。在以下範例中,我們將找到的每個非負整數乘以二。

assert.equal(
  '3 cats and 4 dogs'.replaceAll(/[0-9]+/g, (all) => 2 * Number(all)),
  '6 cats and 8 dogs'
);

替換函式取得以下參數。請注意它們與相符物件有多麼相似。這些參數都是位置參數,但我已包含可以如何命名它們

如果我們只對 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();
  });
assert.equal(result, 'FIRST=JANE, LAST=DOE');

由於第 A 行中的 剩餘參數args 包含一個包含所有參數的陣列。我們透過第 B 行中的 陣列方法 .at() 存取最後一個參數。

43.6.9 處理正規表示式的其他方法

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' ]

43.7 旗標 /g/y,以及屬性 .lastIndex(進階)

在本節中,我們將探討 RegExp 旗標 /g/y 如何運作,以及它們如何依賴 RegExp 屬性 .lastIndex。我們還會發現 .lastIndex 一個有趣的用例,你可能會感到驚訝。

43.7.1 旗標 /g/y

每個方法對 /g/y 的反應都不同;這讓我們對一般情況有粗略的了解

如果正規表示式既沒有旗標 /g 也不沒有旗標 /y,比對會發生一次並從開頭開始。

使用 /g/y 時,比對會相對於輸入字串中的「目前位置」執行。該位置儲存在正規表示式屬性 .lastIndex 中。

有 3 組與正規表示式相關的方法

  1. 字串方法 .search(regExp).split(regExp) 完全忽略 /g/y(因此也忽略 .lastIndex)。

  2. 如果設定 /g/yRegExp 方法 .exec(str).test(str) 會以兩種方式變更。

    首先,我們可以重複呼叫一個方法來取得多個比對。每次呼叫都會傳回另一個結果(比對物件或 true)或「結果結束」值(nullfalse)。

    其次,正規表示式屬性 .lastIndex 用於逐步處理輸入字串。一方面,.lastIndex 決定比對從何處開始

    • /g 表示比對必須從 .lastIndex 或之後開始。

    • /y 表示比對必須從 .lastIndex 開始。也就是說,正規表示式的開頭會固定在 .lastIndex

      請注意,^$ 仍會像平常一樣運作:它們會將比對固定在輸入字串的開頭或結尾,除非設定 .multiline。然後它們會固定在行的開頭或結尾。

    另一方面,.lastIndex 會設定為前一個比對的最後一個索引值加一。

  3. 所有其他方法會受到以下影響

    • /g 會產生多個比對。
    • /y 會產生一個單一比對,且必須從 .lastIndex 開始。
    • /yg 會產生沒有間隙的多個比對。

這是第一個概觀。後面的章節會深入探討更多細節。

43.7.2 方法會如何受到 /g/y 影響?

43.7.2.1 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 相同。

43.7.2.2 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 相同。

43.7.2.3 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.lastIndex
0

/yg 的作用與 /g 相同,但比對之間沒有間隙

> const re = /#/yg; re.lastIndex = 1;
> '##-#'.match(re)
['#', '#']
> re.lastIndex
0
43.7.2.4 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.lastIndex
1

如果設定了 /yg,行為與使用 /g 相同,但比對之間沒有間隙

> const re = /#/yg; re.lastIndex = 1;
> Array.from('##-#'.matchAll(re))
[
  { 0: '#', index: 1, input: '##-#' },
]
> re.lastIndex
1
43.7.2.5 str.replace(regExp, str) [ES3]

不使用 /g/y 時,只會取代第一次出現的字串

> const re = /#/; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'x#-#'
> re.lastIndex
1

使用 /g 時,會取代所有出現的字串。.lastIndex 會被忽略,但會重設為零。

> const re = /#/g; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-x'
> re.lastIndex
0

使用 /y 時,只會取代 .lastIndex 處的(第一次)出現的字串。.lastIndex 會更新。

> const re = /#/y; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'#x-#'
> re.lastIndex
2

/yg 的作用與 /g 相同,但比對之間不允許有間隙

> const re = /#/yg; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-#'
> re.lastIndex
0
43.7.2.6 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

43.7.3 /g/y 的四個陷阱以及如何處理

我們將先探討 /g/y 的四個陷阱,然後探討如何處理這些陷阱。

43.7.3.1 陷阱 1:我們無法將正規表示式內嵌在 /g/y

無法將正規表示式內嵌在 /g 中。例如,在以下 while 迴圈中,每次檢查條件時都會重新建立正規表示式。因此,它的 .lastIndex 永遠都是零,迴圈永遠不會終止。

let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
  console.log(matchObj[0]);
}

使用 /y 時,問題相同。

43.7.3.2 陷阱 2:移除 /g/y 可能會損壞程式碼

如果程式碼預期一個帶有 /g 的正規表示式,並且對 .exec().test() 的結果進行迴圈,那麼沒有 /g 的正規表示式可能會導致無限迴圈

function collectMatches(regExp, str) {
  const matches = [];
  let matchObj;
  // Infinite loop
  while (matchObj = regExp.exec(str)) {
    matches.push(matchObj[0]);
  }
  return matches;
}
collectMatches(/a+/, 'bbbaabaaa'); // Missing: flag /g

為什麼會發生無限迴圈?因為 .exec() 永遠會傳回第一個結果,一個匹配物件,而不會是 null

使用 /y 時,問題相同。

43.7.3.3 陷阱 3:新增 /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
43.7.3.4 陷阱 4:如果 .lastIndex 不是零,程式碼可能會產生意外的結果

由於所有會受到 .lastIndex 影響的正規表示式運算,我們必須小心許多演算法,在開始時 .lastIndex 為零。否則,我們可能會得到意外的結果

function countMatches(regExp, str) {
  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(
  countMatches(myRegExp, 'babaa'), 1); // should be 3

通常,在新建的正規表示式中 .lastIndex 為零,而且我們不會像範例中那樣明確地改變它。但是,如果我們多次使用正規表示式,.lastIndex 仍然可能不會為零。

43.7.3.5 如何避免 /g/y 的陷阱

作為處理 /g.lastIndex 的範例,我們重新檢視前一個範例中的 countMatches()。我們如何防止錯誤的正規表示式損壞我們的程式碼?讓我們來看三種方法。

43.7.3.5.1 拋出例外

首先,如果沒有設定 /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;
}
43.7.3.5.2 複製正規表示式

其次,我們可以複製參數。這樣的好處是 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;
}
43.7.3.5 使用不受 .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;
myRegExp.lastIndex = 4;
assert.equal(countMatches(myRegExp, 'babaa'), 3); // OK!

在此,即使我們沒有檢查或修正 .lastIndexcountMatches() 仍會運作。

43.7.4 .lastIndex 的使用案例:從給定的索引開始比對

除了儲存狀態,.lastIndex 也可用於從給定的索引開始比對。本節說明如何執行。

43.7.4.1 範例:檢查正規表示式是否在給定的索引比對

由於 .test()/y.lastIndex 影響,我們可以使用它來檢查正規表示式 regExp 是否在給定的 index 比對字串 str

function matchesStringAt(regExp, str, index) {
  if (!regExp.sticky) {
    throw new Error('Flag /y of regExp must be set');
  }
  regExp.lastIndex = index;
  return regExp.test(str);
}
assert.equal(
  matchesStringAt(/x+/y, 'aaxxx', 0), false);
assert.equal(
  matchesStringAt(/x+/y, 'aaxxx', 2), true);

由於 /yregExp 會錨定至 .lastIndex

請注意,我們不得使用會將 regExp 錨定至輸入字串開頭的斷言 ^

43.7.4.2 範例:從給定的索引開始尋找比對位置

.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');
  }
  regExp.lastIndex = index;
  const match = regExp.exec(str);
  if (match) {
    return match.index;
  } else {
    return -1;
  }
}

assert.equal(
  searchAt(/#/g, '#--#', 0), 0);
assert.equal(
  searchAt(/#/g, '#--#', 1), 3);
43.7.4.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');
  }
  regExp.lastIndex = index;
  return str.replace(regExp, replacement);
}
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 0), 'X aaaa a');
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 3), 'aa X a');
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 8), 'aa aaaa X');

43.7.5 .lastIndex 的缺點

正規表示式屬性 .lastIndex 有兩個重大的缺點

好處是,.lastIndex 也為我們提供了其他有用的功能:我們可以指定比對應從何處開始(對於某些運算)。

43.7.6 摘要:.global (/g) 和 .sticky (/y)

下列兩個方法完全不受 /g/y 影響

此表格說明其餘正規表示式相關方法如何受這兩個旗標影響

/ /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 = '##-#';

縮寫

  產生前述表格的 Node.js 程式碼

前述表格是透過 Node.js 程式碼 產生的。

43.8 使用正規表示式的技巧

43.8.1 轉譯任意文字以供正規表示式使用

下列函式會轉譯任意文字,以便在正規表示式中使用時,能逐字比對

function escapeForRegExp(str) {
  return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); // (A)
}
assert.equal(escapeForRegExp('[yes?]'), String.raw`\[yes\?\]`);
assert.equal(escapeForRegExp('_g_'), String.raw`_g_`);

在 A 行,我們轉譯所有語法字元。我們必須有選擇性,因為正規表示式旗標 /u 禁止許多轉譯字元,例如:\a \: \-

escapeForRegExp() 有兩個使用案例

.replace() 僅允許我們取代純文字一次。透過 escapeForRegExp(),我們可以解決此限制

const plainText = ':-)';
const regExp = new RegExp(escapeForRegExp(plainText), 'ug');
assert.equal(
  ':-) :-) :-)'.replace(regExp, '🙂'), '🙂 🙂 🙂');

43.8.2 比對所有或無任何內容

有時,我們可能需要一個比對所有或無任何內容的正規表示式,例如,作為預設值。