## 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)
> ![](https://img.kancloud.cn/83/6f/836f0345de4fda5b9b81d7038ebb276d.svg)**功能的可用性**
> 除非另有说明,否则 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)。
![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:通过`.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`:命名捕获组(对象)
![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:通过`.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.其余章节在哪里?