## 39. 正則表達式(`RegExp`)
> 原文: [http://exploringjs.com/impatient-js/ch_regular-expressions.html](http://exploringjs.com/impatient-js/ch_regular-expressions.html)
>
> 貢獻者:[iChrisJ](https://github.com/iChrisJ)
> **功能的可用性**
> 除非另有說明,否則 ES5 及更高版本支持所有正則表達式功能。
### 39.1. 創建正則表達式
#### 39.1.1. 字面義式 vs. 構造函數式
創建正則表達式的兩種主要方法是:
* 字面義式:`/abc/ui`,被靜態編譯(在加載時)。
* 構造函數式:`new RegExp('abc', 'ui')`,被動態編譯(在運行時)
* 第二個參數是可選的。
兩個正則表達式都有相同的兩個部分:
* _表達式體_ `abc` - 實際正則表達式。
* _標志_ `u` 和 `i`。標志配置了表達式模式如何被解釋:`u`切換到 [_Unicode 模式 _](ch_regular-expressions.html#regexp-unicode-mode) 。 `i`啟用大小寫不敏感匹配。
#### 39.1.2. 克隆和非破壞性地修改正則表達式
構造函數`RegExp()`有兩種變體:
* `new RegExp(pattern : string, flags = '')`
通過指定的`pattern`創建新的正則表達式。如果缺少`flags`,則使用空字符串`''`。
* `new RegExp(regExp : RegExp, flags = regExp.flags)`<sup>[ES6]</sup>
`regExp`被克隆。如果提供了`flags`,則它確定副本的標志。
第二個變體可用于克隆正則表達式,在修改它們時也是可選的。標志是不可變的,這是改變它們的唯一方法。例如:
```JavaScript
function copyAndAddFlags(regExp, flags='') {
// The constructor doesn’t allow duplicate flags,
// make sure there aren’t any:
const newFlags = [...new Set(regExp.flags + flags)].join('');
return new RegExp(regExp, newFlags);
}
assert.equal(/abc/i.flags, 'i');
assert.equal(copyAndAddFlags(/abc/i, 'g').flags, 'gi');
```
### 39.2. 語法
#### 39.2.1. 語法字符
在正則表達式的頂層,以下 _語法字符_ 是特殊的。它們通過前綴反斜杠(`\`)進行轉義。
```JavaScript
\ ^ $ . * + ? ( ) [ ] { } |
```
在正則表達式字面義中,您還必須轉義斜杠(用`new RegExp()`就沒有必要了):
```JavaScript
> /\//.test('/')
true
> new RegExp('/').test('/')
true
```
#### 39.2.2. 基本原子
_原子_ 是正則表達式的基本構建塊。
* 模式字符:除語法字符(`^`,`$`等)外的所有字符。模式字符匹配自己。示例:`A b %`
* `.`匹配任何字符。您可以使用標志`/s`(`dotall`)來控制點`.`是否與行終止符匹配([下面詳述](ch_regular-expressions.html#reg-exp-flags))。
* 字符轉義(每個轉義匹配一個固定字符):
* 控制轉義(用于一些控制字符):
* `\f`:換頁(FF)
* `\n`:換行(LF)
* `\r`:回車(CR)
* `\t`:角色列表
* `\v`:行列表
* 任意控制字??符:`\cA`(Ctrl-A),`\cB`(Ctrl-B)等。
* Unicode 代碼單位:`\u00E4`
* Unicode 代碼點(需要標志`/u`):`\u{1F44D}`
* 字符類轉義(每個轉義匹配一組字符中的一個):
* `\d`:數字(與`[0-9]`相同)
* `\D`:非數字
* `\w`:“單詞”字符(與`[A-Za-z0-9_]`相同)
* `\W`:非單詞字符
* `\s`:空白(空格,制表符,行終止符等)
* `\S`:非空白
* Unicode 屬性轉義(ES2018):`\p{White_Space}`,`\P{White_Space}`等.
* 需要標志`/u`。
* 在下一小節中描述。
##### 39.2.2.1. 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 的所有字符。
評論:
* 如果設置了標志`/u`,則只能使用 Unicode 屬性轉義。沒有`/u`,`\p`與`p`相同。
* 如果屬性是`General_Category`,則表格(3)和(4)可以用作縮寫。例如,`\p{Lowercase_Letter}`是`\p{General_Category=Lowercase_Letter}`的縮寫
例子:
* 檢查空格:
```JavaScript
> /^\p{White_Space}+$/u.test('\t \n\r')
true
```
* 檢查希臘字母:
```JavaScript
> /^\p{Script=Greek}+$/u.test('μετ?')
true
```
* 刪除任何字母:
```JavaScript
> '1π2ü3é4'.replace(/\p{Letter}/ug, '')
'1234'
```
* 刪除小寫字母:
```JavaScript
> 'AbCdEf'.replace(/\p{Lowercase_Letter}/ug, '')
'ACE'
```
進一步閱讀:
* Unicode 屬性及其值的列表:[“Unicode 標準附件#44:Unicode 字符數據庫”](https://unicode.org/reports/tr44/#Properties)(編輯:Mark Davis,Lauren?iuIancu,Ken Whistler)
* Unicode 屬性更深入地轉義:“探索 ES2018 和 ES2019”中的章節[“RegEx Unicode 屬性轉義”](http://exploringjs.com/es2018-es2019/ch_regexp-unicode-property-escapes.html)
#### 39.2.3。角色類
* 匹配一組字符中的一個:`[abc]`
* 匹配不在集合中的任何字符:`[^abc]`
* 在方括號內,只有以下字符是特殊的,必須進行轉義:
```JavaScript
^ \ - ]
```
`^`只有先到時才需要進行轉義。 `-`如果是第一個或最后一個,則無需轉義
* 字符轉義(`\n`,`\u{1F44D}`)和字符類轉義(`\d`,`\p{White_Space}`)照常工作。
* 例外:在方括號內,`\b`匹配退格。在其他地方,它匹配單詞邊界。
* 字符范圍通過短劃線指定:`[a-z]`,`[^a-z]`
#### 39.2.4. 組
* 位置捕獲組:`(#+)`
* 反向引用:`\1`,`\2`等
* 命名捕獲組(ES2018):`(?<hashes>#+)`
* 反向引用:`\k<hashes>`
* 非捕獲組:`(?:#+)`
#### 39.2.5. 量詞
默認情況下,以下所有量詞都是貪心的:
* `?`:匹配從不或一次
* `*`:匹配零次或多次
* `+`:匹配一次或多次
* `{n}`:匹配`n`次
* `{n,}`:匹配`n`次或更多次
* `{n,m}`:至少匹配`n`次,最多`m`次。
為了使他們不情愿,在他們后面加上問號(`?`):
```JavaScript
> /".*"/.exec('"abc"def"')[0] // greedy
'"abc"def"'
> /".*?"/.exec('"abc"def"')[0] // reluctant
'"abc"'
```
#### 39.2.6. 斷言
* `^`僅在輸入的開頭匹配
* `$`僅在輸入結束時匹配
* `\b`僅匹配單詞邊界
* `\B`僅在不在單詞邊界時匹配
* 向前看:
* 如果`pattern`匹配下一個(積極前瞻),則`(?=?pattern?)`匹配。示例(“`X`后跟小寫字母的序列” - 請注意`X`本身不是匹配的子字符串的一部分):
```JavaScript
> 'abcX def'.match(/[a-z]+(?=X)/g)
[ 'abc' ]
```
* 如果`pattern`與接下來的內容不匹配,則`(?!?pattern?)`匹配(消極前瞻)。示例(“小寫字母的序列,后面沒有`X`”)
```JavaScript
> 'abcX def'.match(/[a-z]+(?!X)/g)
[ 'ab', 'def' ]
```
* 進一步閱讀:[“探索 ES2018 和 ES2019”中的“RegExp lookbehind 斷言”](http://exploringjs.com/es2018-es2019/ch_regexp-lookbehind-assertions.html)(也涵蓋了先行斷言)
* 向后看 (ES2018):
* 如果`pattern`與之前的相符,則`(?<=?pattern?)`匹配(積極后觀)
```JavaScript
> 'Xabc def'.match(/(?<=X)[a-z]+/g)
[ 'abc' ]
```
* 如果`pattern`與之前的不匹配,則`(?<!?pattern?)`匹配(消極后觀)
```JavaScript
> 'Xabc def'.match(/(?<!X)[a-z]+/g)
[ 'bc', 'def' ]
```
* 進一步閱讀:[“探索 ES2018 和 ES2019”中的“RegExp lookbehind 斷言”](http://exploringjs.com/es2018-es2019/ch_regexp-lookbehind-assertions.html)
#### 39.2.7. 分離(`|`)
警告:此運算符的優先級較低。必要時使用組:
* `^aa|zz$`匹配以`aa`開頭和/或以`zz`結束的所有字符串。請注意,`|`的優先級低于`^`和`$`。
* `^(aa|zz)$`匹配兩個字符串`'aa'`和`'zz'`。
* `^a(a|z)z$`匹配兩個字符串`'aaz'`和`'azz'`。
### 39.3. 標志
表格 20: 這些是JavaScript支持的正則表達式標志。
| 字面義標志 | 屬性名稱 | ES | 描述 |
| --- | --- | --- | --- |
| `g` | `global` | ES3 | 匹配多次 |
| `i` | `ignoreCase` | ES3 | 不區分大小寫 |
| `m` | `multiline` | ES3 | 每行`^`和`$`匹配 |
| `s` | `dotall` | ES2018 | 點`.`匹配行終止符 |
| `u` | `unicode` | ES6 | Unicode 模式(推薦) |
| `y` | `sticky` | ES6 | 匹配之間沒有字符 |
JavaScript 中提供了以下正則表達式標志(表格[20](#tbl:reg-exp-flags-table) 提供了緊湊的概述):
* `/g`(`.global`):從根本上改變方法`RegExp.prototype.test()`,`RegExp.prototype.exec()`和`String.prototype.match()`的工作方式。將與這些方法一起詳細解釋。簡而言之:如果沒有`/g`,方法只考慮輸入字符串中正則表達式的第一個匹配項。使用`/g`,他們會考慮所有匹配。
* `/i`(`.ignoreCase`):打開不區分大小寫的匹配:
```JavaScript
> /a/.test('A')
false
> /a/i.test('A')
true
```
* `/m`(`.multiline`):如果該標志打開,`^`匹配每一行的開頭,`$`匹配每一行的結尾。如果它關閉,`^`匹配整個輸入字符串的開頭,`$`匹配整個輸入字符串的結尾。
```JavaScript
> 'a1\na2\na3'.match(/^a./gm)
[ 'a1', 'a2', 'a3' ]
> 'a1\na2\na3'.match(/^a./g)
[ 'a1' ]
```
* `/u`(`.unicode`):該標志用于打開正則表達式的 Unicode 模式。該模式將在下一小節中解釋。
* `/y`(`.sticky`):該標志僅與`/g`一起使用。當兩者都打開時,第一個之后的任何匹配必須直接跟隨前一個匹配(它們之間沒有任何字符)。
```JavaScript
> 'a1a2 a3'.match(/a./gy)
[ 'a1', 'a2' ]
> 'a1a2 a3'.match(/a./g)
[ 'a1', 'a2', 'a3' ]
```
* `/s`(`.dotall`):默認情況下,點與行終止符不匹配。有了這個標志,它確實:
```JavaScript
> /./.test('\n')
false
> /./s.test('\n')
true
```
舊版 ECMAScript 版本的替代方案:
```JavaScript
> /[^]/.test('\n')
true
```
#### 39.3.1. 標志:通過`/u`的 Unicode 模式
標志`/u`為正則表達式打開特殊的 Unicode 模式。該模式支持多種功能:
* 在模式中,您可以使用 Unicode 代碼點轉義(例如`\u{1F42A}`)來指定字符。諸如`\u03B1`之類的代碼單元轉義只有四個十六進制數字的范圍(等于基本的多語言平面)。
* 在模式中,您可以使用 Unicode 屬性轉義(ES2018),例如`\p{White_Space}`。
* 現在禁止許多轉義(這使得之前的功能成為可能):
```JavaScript
> /\a/
/\a/
> /\a/u
SyntaxError: Invalid regular expression: /\a/: Invalid escape
> /\-/
/\-/
> /\-/u
SyntaxError: Invalid regular expression: /\-/: Invalid escape
> /\:/
/\:/
> /\:/u
SyntaxError: Invalid regular expression: /\:/: Invalid escape
```
* 匹配的原子單位(“字符”)是代碼點,而不是代碼單元。
以下小節將更詳細地解釋最后一項。它們使用以下 Unicode 字符來解釋原子單位何時是代碼點以及何時是代碼單元:
```JavaScript
const codePoint = '??';
const codeUnits = '\uD83D\uDE42'; // UTF-16
assert.equal(codePoint, codeUnits); // same string!
```
我只在`??`和`\uD83D\uDE42`之間切換,以說明 JavaScript 如何看待事物。兩者都是等價的,可以在字符串和正則表達式中互換使用。
##### 39.3.1.1. 結果:您可以將代碼點放在字符類中
使用`/u`,`??`的兩個代碼單元被解釋為單個字符:
```JavaScript
> /^[??]$/u.test('??')
true
```
沒有`/u`,`??`被解釋為兩個字符:
```JavaScript
> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42')
false
> /^[\uD83D\uDE42]$/.test('\uDE42')
true
```
請注意,`^`和`$`要求輸入字符串具有單個字符。這就是為什么第一個結果是`false`。
##### 39.3.1.2. 結果:點運算符(`.`)匹配代碼點,而不是代碼單元
使用`/u`,點運算符匹配代碼點(`.match()`加`/g`返回一個包含正則表達式的所有匹配項的數組):
```JavaScript
> '??'.match(/./gu).length
1
```
沒有`/u`,點運算符匹配單個代碼單元:
```JavaScript
> '\uD83D\uDE80'.match(/./g).length
2
```
##### 39.3.1.3. 后果:量詞適用于代碼點,而不是代碼單元
使用`/u`,量詞適用于整個前面的代碼點:
```JavaScript
> /^??{3}$/u.test('??????')
true
```
沒有`/u`,量詞僅適用于前面的代碼單元:
```JavaScript
> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80')
true
```
### 39.4. 正則表達式對象的屬性
值得注意的是:
* 嚴格地說,只有`.lastIndex`是一個真實的實例屬性。所有其他屬性都通過 getter 實現。
* 因此,`.lastIndex`是唯一可變的屬性。所有其他屬性都是只讀的。如果要更改它們,則需要復制正則表達式(有關詳細信息,請參閱[克隆部分](ch_regular-expressions.html#cloning-regexps))。
#### 39.4.1. 標志作為屬性
每個正則表達式標志都作為屬性存在,具有更長,更具描述性的名稱:
```JavaScript
> /a/i.ignoreCase
true
> /a/.ignoreCase
false
```
這是標志屬性的完整列表:
* `.dotall`(`/s`)
* `.global`(`/g`)
* `.ignoreCase`(`/i`)
* `.multiline`(`/m`)
* `.sticky`(`/y`)
* `.unicode`(`/u`)
#### 39.4.2. 其他財產
每個正則表達式還具有以下屬性:
* `.source`:正則表達式模式。
```JavaScript
> /abc/ig.source
'abc'
```
* `.flags`:正則表達式的標志。
```JavaScript
> /abc/ig.flags
'gi'
```
* `.lastIndex`:當標志`/g`打開時使用。有關詳細信息,請參閱[關于該標志](ch_regular-expressions.html#regexp-flag-g)的部分。
### 39.5. 使用正則表達式的方法
#### 39.5.1. `regExp.test(str)`:有匹配嗎?
如果`regExp`與`str`匹配,則正則表達式方法`.test()`返回`true`:
```JavaScript
> /abc/.test('ABC')
false
> /abc/i.test('ABC')
true
> /\.js$/.test('main.js')
true
```
使用`.test()`時,通常應避免使用`/g`標志。如果您使用它,每次調用方法時通常不會得到相同的結果:
```JavaScript
> const r = /a/g;
> r.test('aab')
true
> r.test('aab')
true
> r.test('aab')
false
```
結果是由于`/a/`在字符串中有兩個匹配項。找到所有這些后,`.test()`返回`false`。
#### 39.5.2. `str.search(regExp)`:匹配的是什么指數?
字符串方法`.search()`返回`regExp`的第一個索引,其中`regExp`匹配:
```JavaScript
> '_abc_'.search(/abc/)
1
> 'main.js'.search(/\.js$/)
4
```
#### 39.5.3. `regExp.exec(str)`:捕獲組
##### 39.5.3.1. 獲取第一個匹配項的匹配對象
如果沒有標志`/g`,`.exec()`將返回`str`中`regExp`的第一個匹配的所有捕獲:
```JavaScript
assert.deepEqual(
/(a+)b/.exec('ab aab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: undefined,
}
);
```
結果是 _ 匹配對象 _ 具有以下屬性:
* `[0]`:正則表達式匹配的完整子字符串
* `[1]`:位置捕獲組 1(等)
* `.index`:匹配發生在哪里?
* `.input`:匹配的字符串
* `.groups`:命名捕獲組
##### 39.5.3.2. 命名組(ES2018)
前一個示例包含一個位置組。以下示例演示了命名組:
```JavaScript
const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/u;
assert.deepEqual(
regExp.exec('first: Jane'),
{
0: 'first: Jane',
1: 'first',
2: 'Jane',
index: 0,
input: 'first: Jane',
groups: { key: 'first', value: 'Jane' },
}
);
```
如您所見,命名組`key`和`value`也作為位置組存在。
##### 39.5.3.3. 遍歷多個匹配項
如果要檢索正則表達式的所有匹配(而不僅僅是第一個),則需要打開標志`/g`。然后你可以多次調用`.exec()`并每次獲得另一個匹配項。在最后一個匹配項之后,`.exec()`返回`null`。
```JavaScript
> 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
```
因此,您可以循環所有匹配,如下所示:
```JavaScript
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`共享正則表達式有一些陷阱,[稍后會解釋](ch_regular-expressions.html#regexp-flag-g)。
 **練習:通過`.exec()`** 提取引用文本
`exercises/reg-exp/extract_quoted_test.js`
#### 39.5.4. `str.match(regExp)`:返回所有匹配的子串
沒有`/g`,`.match()`就像`.exec()`一樣 - 它返回一個匹配對象。
使用`/g`,`.match()`返回與`regExp`匹配的`str`的所有子串:
```JavaScript
> 'ab aab'.match(/(a+)b/g) // important: /g
[ 'ab', 'aab' ]
```
如果沒有匹配,`.match()`返回`null`:
```JavaScript
> 'xyz'.match(/(a+)b/g)
null
```
您可以使用 Or 運算符來保護自己免受`null`的影響:
```JavaScript
const numberOfMatches = (str.match(regExp) || []).length;
```
#### 39.5.5. `str.replace(searchValue, replacementValue)`
`.replace()`有幾種不同的模式,具體取決于您為其參數提供的值:
* `searchValue`是......
* 沒有`/g`的正則表達式:替換第一次出現。
* 帶`/g`的正則表達式:替換所有出現的事件。
* 字符串:替換第一次出現(字符串逐字解釋,而不是正則表達式)。唉,這意味著字符串作為搜索值的用途有限。在本章的后面,你會發現[是一個工具函數,用于將任意文本轉換為正則表達式](ch_regular-expressions.html#escapeForRegExp)。
* `replacementValue`是......
* 字符串:描述替換
* 功能:計算替換
下一小節假設正在使用帶有`/g`的正則表達式。
##### 39.5.5.1. `replacementValue`是一個字符串
如果替換值是字符串,則美元符號具有特殊含義 - 它插入與正則表達式匹配的內容:
| 文本 | 結果 |
| --- | --- |
| `$$` | 單個`$` |
| `$&` | 完整匹配項 |
| `$`` | 匹配項前的文字 |
| `$'` | 匹配項后的文字 |
| `$n` | 位置捕獲組`n`(`n > 0`) |
| `$<name>` | 命名捕獲組`name` |
示例:在匹配的子字符串之前,之內和之后插入文本。
```JavaScript
> 'a1 a2'.replace(/a/g, "($`|$&|$')")
'(|a|1 a2)1 (a1 |a|2)2'
```
示例:插入位置捕獲組。
```JavaScript
> const regExp = /^([A-Za-z]+): (.*)$/ug;
> 'first: Jane'.replace(regExp, 'KEY: $1, VALUE: $2')
'KEY: first, VALUE: Jane'
```
示例:插入命名捕獲組。
```JavaScript
> const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/ug;
> 'first: Jane'.replace(regExp, 'KEY: $<key>, VALUE: $<value>')
'KEY: first, VALUE: Jane'
```
##### 39.5.5.2. `replacementValue`是一個功能
如果替換值是函數,則可以計算每個替換值。在下面的示例中,我們將我們找到的每個非負整數乘以 2。
```JavaScript
assert.equal(
'3 cats and 4 dogs'.replace(/[0-9]+/g, (all) => 2 * Number(all)),
'6 cats and 8 dogs'
);
```
替換函數獲取以下參數。請注意它們與匹配對象的相似程度。這些參數都是位置的,但我已經包含了通常如何命名它們:
* `all`:完全匹配
* `g1`:位置捕獲組 1
* 等等。
* `index`:匹配發生在哪里?
* `input`:匹配的字符串
* `groups`:命名捕獲組(對象)
 **練習:通過`.replace()`和命名組**更改引號
`exercises/reg-exp/change_quotes_test.js`
#### 39.5.6. 使用正則表達式的其他方法
`String.prototype.split()`的第一個參數是字符串或正則表達式。如果是后者,則將組捕獲的子串添加到方法的結果中:
```JavaScript
> 'a : b : c'.split(/( *):( *)/)
[ 'a', ' ', ' ', 'b', ' ', ' ', 'c' ]
```
有關更多信息,請參閱[有關字符串](ch_strings.html#string-api-extracting)的章節。
### 39.6. 標志`/g`及其陷阱
如果打開`/g`,以下兩個正則表達式方法會執行異常操作:
* `RegExp.prototype.exec()`
* `RegExp.prototype.test()`
然后可以重復調用它們并在字符串中傳遞所有匹配項。正則表達式的屬性`.lastIndex`用于跟蹤字符串中的當前位置。例如:
```JavaScript
const r = /a/g;
assert.equal(r.lastIndex, 0);
assert.equal(r.test('aa'), true); // 1st match?
assert.equal(r.lastIndex, 1); // after 1st match
assert.equal(r.test('aa'), true); // 2nd match?
assert.equal(r.lastIndex, 2); // after 2nd match
assert.equal(r.test('aa'), false); // 3rd match?
assert.equal(r.lastIndex, 0); // start over
```
那么 flag `/g`怎么會有問題呢?我們將首先探討問題然后解決問題。
#### 39.6.1. 問題:您無法使用標志`/g`內聯正則表達式
無法內聯帶有`/g`的正則表達式:例如,在以下`while`循環中,每次檢查條件時都會創建正則表達式。因此,它的`.lastIndex`始終為零,循環永遠不會終止。
```JavaScript
let count = 0;
// Infinite loop
while (/a/g.test('babaa')) {
count++;
}
```
#### 39.6.2. 問題:刪除`/g`可能會破壞代碼
如果代碼需要帶有`/g`的正則表達式并且在`.exec()`或`.test()`的結果上有一個循環,那么沒有`/g`的正則表達式會導致無限循環:
```JavaScript
const regExp = /a/; // Missing: flag /g
let count = 0;
// Infinite loop
while (regExp.test('babaa')) {
count++;
}
```
為什么?因為`.test()`總是返回第一個結果,`true`,而不是`false`。
#### 39.6.3. 問題:添加`/g`可能會破壞代碼
使用`.test()`時,還有另一個警告:如果要檢查正則表達式是否與字符串匹配,則正則表達式必須不具有`/g`。否則,每次調用`.test()`時,通常會得到不同的結果:
```JavaScript
> const r = /^X/g;
> r.test('Xa')
true
> r.test('Xa')
false
```
通常,如果您打算以這種方式使用`.test()`,則不會添加`/g`。但是,例如,如果您使用相同的正則表達式進行測試和替換,則會發生這種情況。或者,如果您通過參數獲得正則表達式。
#### 39.6.4. 問題:如果`.lastIndex`不為零,代碼可能會中斷
創建正則表達式時,`.lastIndex`初始化為零。如果代碼曾經收到`.lastIndex`不為零的正則表達式,它可能會中斷。例如:
```JavaScript
const regExp = /a/g;
regExp.lastIndex = 4;
let count = 0;
while (regExp.test('babaa')) {
count++;
}
assert.equal(count, 1); // should be 3
```
如果正則表達式被共享且未正確處理,則`.lastIndex`不為零可能相對容易發生。
#### 39.6.5. 處理`/g`和`.lastIndex`
請考慮以下情形:您想要實現一個函數`countOccurrences(regExp, str)`,它計算`regExp`在`str`內??的匹配頻率。你如何防止錯誤的`regExp`破壞你的代碼?我們來看看三種方法。
首先,如果未設置`/g`或`.lastIndex`不為零,則可以拋出異常:
```JavaScript
function countOccurrences(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`不會改變。
```JavaScript
function countOccurrences(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;
}
```
第三,您可以使用`.match()`計算出現次數 - 這些次數不會改變或取決于`.lastIndex`。
```JavaScript
function countOccurrences(regExp, str) {
if (!regExp.global) {
throw new Error('Flag /g of regExp must be set');
}
return (str.match(regExp) || []).length;
}
```
### 39.7. 使用正則表達式的技巧
#### 39.7.1. 轉義正則表達式的任意文本
以下函數會轉義任意文本,以便在將其放入正則表達式中時逐字匹配:
```JavaScript
function escapeForRegExp(str) {
return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); // (A)
}
assert.equal(escapeForRegExp('[yes?]'), String.raw`\[yes\?\]`);
assert.equal(escapeForRegExp('_g_'), String.raw`_g_`);
```
在 A 行中,我們轉義所有語法字符。請注意,`/u`禁止許多逃逸:其中包括`\:`和`\-`。
這是您可以使用`escapeForRegExp()`多次替換任意文本的方法:
```JavaScript
> const re = new RegExp(escapeForRegExp(':-)'), 'ug');
> ':-) :-) :-)'.replace(re, '??')
'?? ?? ??'
```
#### 39.7.2. 匹配一切或什么也沒有
有時,您可能需要一個匹配所有內容的正則表達式。例如,作為標記值。
* 匹配所有內容:`/(?:)/`(空組匹配所有內容;使其無法捕獲,避免不必要的工作)
```JavaScript
> /(?:)/.test('')
true
> /(?:)/.test('abc')
true
```
* 什么都不匹配:`/.^/`(一旦匹配進展超出第一個字符,`^`就不再匹配了)
```JavaScript
> /.^/.test('')
false
> /.^/.test('abc')
false
```
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?