37.1. 创建正则表达式
37.1.1. 字面义式 vs. 构造函数式
创建正则表达式的两种主要方法是:
- 字面义式:
/abc/ui
,被静态编译(在加载时)。 - 构造函数式:
new RegExp('abc', 'ui')
,被动态编译(在运行时)- 第二个参数是可选的。
两个正则表达式都有相同的两个部分:
- 表达式体
abc
- 实际正则表达式。 - 标志
u
和i
。标志配置了表达式模式如何被解释:u
切换到 Unicode 模式 。i
启用大小写不敏感匹配。
37.1.2. 克隆和非破坏性地修改正则表达式
构造函数RegExp()
有两种变体:
new RegExp(pattern : string, flags = '')
通过指定的pattern
创建新的正则表达式。如果缺少flags
,则使用空字符串''
。new RegExp(regExp : RegExp, flags = regExp.flags)
^[ES6]^regExp
被克隆。如果提供了flags
,则它确定副本的标志。
第二个变体可用于克隆正则表达式,在修改它们时也是可选的。标志是不可变的,这是改变它们的唯一方法。例如:
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');
37.2. 语法
37.2.1. 语法字符
- 在正则表达式的顶层,以下 语法字符 是特殊的。它们通过前缀反斜杠(
\
)进行转义。\ ^ $ . * + ? ( ) [ ] { } |
- 在正则表达式字面义中,您还必须转义斜杠(用
new RegExp()
就没有必要了):> /\//.test('/') true > new RegExp('/').test('/') true
37.2.2. 基本原子
原子 是正则表达式的基本构建块。
- 模式字符:除语法字符(
^
,$
等)外的所有字符。模式字符匹配自己。示例:A b %
.
匹配任何字符。您可以使用标志/s
(dotall
)来控制点.
是否与行终止符匹配(下面详述)。- 字符转义(每个转义匹配一个固定字符):
- 控制转义(用于一些控制字符):
\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
。 - 在下一小节中描述。
- 需要标志
37.2.2.1. Unicode 属性转义
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{Lowercase_Letter}
是\p{General_Category=Lowercase_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'
进一步阅读:
- Unicode 属性及其值的列表:“Unicode 标准附件#44:Unicode 字符数据库”(编辑:Mark Davis,LaurenţiuIancu,Ken Whistler)
- Unicode 属性更深入地转义:“探索 ES2018 和 ES2019”中的章节“RegEx Unicode 属性转义”
37.2.3。角色类
- 匹配一组字符中的一个:
[abc]
- 匹配不在集合中的任何字符:
[^abc]
- 匹配不在集合中的任何字符:
- 在方括号内,只有以下字符是特殊的,必须进行转义:
^ \ - ]
^
只有先到时才需要进行转义。-
如果是第一个或最后一个,则无需转义 - 字符转义(
\n
,\u{1F44D}
)和字符类转义(\d
,\p{White_Space}
)照常工作。- 例外:在方括号内,
\b
匹配退格。在其他地方,它匹配单词边界。
- 例外:在方括号内,
- 字符范围通过短划线指定:
[a-z]
,[^a-z]
37.2.4. 组
- 位置捕获组:
(#+)
- 反向引用:
\1
,\2
等
- 反向引用:
- 命名捕获组(ES2018):
(?<hashes>#+)
- 反向引用:
\k<hashes>
- 反向引用:
- 非捕获组:
(?:#+)
37.2.5. 量词
默认情况下,以下所有量词都是贪心的:
?
:匹配从不或一次*
:匹配零次或多次+
:匹配一次或多次{n}
:匹配n
次{n,}
:匹配n
次或更多次{n,m}
:至少匹配n
次,最多m
次。
为了使他们不情愿,在他们后面加上问号(?
):
> /".*"/.exec('"abc"def"')[0] // greedy
'"abc"def"'
> /".*?"/.exec('"abc"def"')[0] // reluctant
'"abc"'
37.2.6. 断言
^
仅在输入的开头匹配$
仅在输入结束时匹配\b
仅匹配单词边界\B
仅在不在单词边界时匹配
- 向前看:
- 如果
pattern
匹配下一个(积极前瞻),则(?=«pattern»)
匹配。示例(“X
后跟小写字母的序列” - 请注意X
本身不是匹配的子字符串的一部分):
> 'abcX def'.match(/[a-z]+(?=X)/g) [ 'abc' ]
- 如果
pattern
与接下来的内容不匹配,则(?!«pattern»)
匹配(消极前瞻)。示例(“小写字母的序列,后面没有X
”)
> 'abcX def'.match(/[a-z]+(?!X)/g) [ 'ab', 'def' ]
- 进一步阅读:“探索 ES2018 和 ES2019”中的“RegExp lookbehind 断言”(也涵盖了先行断言)
- 如果
- 向后看 (ES2018):
- 如果
pattern
与之前的相符,则(?<=«pattern»)
匹配(积极后观)
> 'Xabc def'.match(/(?<=X)[a-z]+/g) [ 'abc' ]
- 如果
pattern
与之前的不匹配,则(?<!«pattern»)
匹配(消极后观)
> 'Xabc def'.match(/(?<!X)[a-z]+/g) [ 'bc', 'def' ]
- 如果
37.2.7. 分离(|
)
警告:此运算符的优先级较低。必要时使用组:
^aa|zz$
匹配以aa
开头和/或以zz
结束的所有字符串。请注意,|
的优先级低于^
和$
。^(aa|zz)$
匹配两个字符串'aa'
和'zz'
。^a(a|z)z$
匹配两个字符串'aaz'
和'azz'
。
37.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 提供了紧凑的概述):
/g
(.global
):从根本上改变方法RegExp.prototype.test()
,RegExp.prototype.exec()
和String.prototype.match()
的工作方式。将与这些方法一起详细解释。简而言之:如果没有/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
一起使用。当两者都打开时,第一个之后的任何匹配必须直接跟随前一个匹配(它们之间没有任何字符)。> 'a1a2 a3'.match(/a./gy) [ 'a1', 'a2' ] > 'a1a2 a3'.match(/a./g) [ 'a1', 'a2', 'a3' ]
/s
(.dotall
):默认情况下,点与行终止符不匹配。有了这个标志,它确实:> /./.test('\n') false > /./s.test('\n') true
旧版 ECMAScript 版本的替代方案:
> /[^]/.test('\n')
true
37.3.1. 标志:通过/u
的 Unicode 模式
标志/u
为正则表达式打开特殊的 Unicode 模式。该模式支持多种功能:
- 在模式中,您可以使用 Unicode 代码点转义(例如
\u{1F42A}
)来指定字符。诸如\u03B1
之类的代码单元转义只有四个十六进制数字的范围(等于基本的多语言平面)。 - 在模式中,您可以使用 Unicode 属性转义(ES2018),例如
\p{White_Space}
。 - 现在禁止许多转义(这使得之前的功能成为可能):
> /\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 字符来解释原子单位何时是代码点以及何时是代码单元:
const codePoint = '🙂';
const codeUnits = '\uD83D\uDE42'; // UTF-16
assert.equal(codePoint, codeUnits); // same string!
我只在🙂
和\uD83D\uDE42
之间切换,以说明 JavaScript 如何看待事物。两者都是等价的,可以在字符串和正则表达式中互换使用。
37.3.1.1. 结果:您可以将代码点放在字符类中
- 使用
/u
,🙂
的两个代码单元被解释为单个字符:> /^[🙂]$/u.test('🙂') true
- 没有
/u
,🙂
被解释为两个字符:> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42') false > /^[\uD83D\uDE42]$/.test('\uDE42') true
请注意,^
和$
要求输入字符串具有单个字符。这就是为什么第一个结果是false
。
37.3.1.2. 结果:点运算符(.
)匹配代码点,而不是代码单元
- 使用
/u
,点运算符匹配代码点(.match()
加/g
返回一个包含正则表达式的所有匹配项的数组):> '🙂'.match(/./gu).length 1
- 没有
/u
,点运算符匹配单个代码单元:> '\uD83D\uDE80'.match(/./g).length 2
37.3.1.3. 后果:量词适用于代码点,而不是代码单元
- 使用
/u
,量词适用于整个前面的代码点:> /^🙂{3}$/u.test('🙂🙂🙂') true
- 没有
/u
,量词仅适用于前面的代码单元:> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80') true
37.4. 正则表达式对象的属性
值得注意的是:
- 严格地说,只有
.lastIndex
是一个真实的实例属性。所有其他属性都通过 getter 实现。 - 因此,
.lastIndex
是唯一可变的属性。所有其他属性都是只读的。如果要更改它们,则需要复制正则表达式。
37.4.1. 标志作为属性
每个正则表达式标志都作为属性存在,具有更长,更具描述性的名称:
> /a/i.ignoreCase
true
> /a/.ignoreCase
false
这是标志属性的完整列表:
.dotall
(/s
).global
(/g
).ignoreCase
(/i
).multiline
(/m
).sticky
(/y
).unicode
(/u
)
37.4.2. 其他财产
每个正则表达式还具有以下属性:
.source
:正则表达式模式。> /abc/ig.source 'abc'
.flags
:正则表达式的标志。> /abc/ig.flags 'gi'
.lastIndex
:当标志/g
打开时使用。
37.5. 使用正则表达式的方法
37.5.1. regExp.test(str)
:有匹配吗?
- 如果
regExp
与str
匹配,则正则表达式方法.test()
返回true
:> /abc/.test('ABC') false > /abc/i.test('ABC') true > /\.js$/.test('main.js') true
- 使用
.test()
时,通常应避免使用/g
标志。如果您使用它,每次调用方法时通常不会得到相同的结果:> const r = /a/g; > r.test('aab') true > r.test('aab') true > r.test('aab') false
- 结果是由于
/a/
在字符串中有两个匹配项。找到所有这些后,.test()
返回false
。
37.5.2. str.search(regExp)
:匹配的是什么指数?
- 字符串方法
.search()
返回regExp
的第一个索引,其中regExp
匹配:> '_abc_'.search(/abc/) 1 > 'main.js'.search(/\.js$/) 4
37.5.3. regExp.exec(str)
:捕获组
37.5.3.1. 获取第一个匹配项的匹配对象
- 如果没有标志
/g
,.exec()
将返回str
中regExp
的第一个匹配的所有捕获:assert.deepEqual( /(a+)b/.exec('ab aab'), { 0: 'ab', 1: 'a', index: 0, input: 'ab aab', groups: undefined, } );
结果是 _ 匹配对象 _ 具有以下属性:
[0]
:正则表达式匹配的完整子字符串[1]
:位置捕获组 1(等).index
:匹配发生在哪里?.input
:匹配的字符串.groups
:命名捕获组
37.5.3.2. 命名组(ES2018)
- 前一个示例包含一个位置组。以下示例演示了命名组:
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
也作为位置组存在。
37.5.3.3. 遍历多个匹配项
- 如果要检索正则表达式的所有匹配(而不仅仅是第一个),则需要打开标志
/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
共享正则表达式有一些陷阱,稍后会解释。
练习:通过
.exec()
提取引用文本
exercises/reg-exp/extract_quoted_test.js
37.5.4. str.match(regExp)
:返回所有匹配的子串
- 没有
/g
,.match()
就像.exec()
一样 - 它返回一个匹配对象。使用/g
,.match()
返回与regExp
匹配的str
的所有子串:> 'ab aab'.match(/(a+)b/g) // important: /g [ 'ab', 'aab' ]
- 如果没有匹配,
.match()
返回null
:> 'xyz'.match(/(a+)b/g) null
- 您可以使用 Or 运算符来保护自己免受
null
的影响:const numberOfMatches = (str.match(regExp) || []).length;
37.5.5. str.replace(searchValue, replacementValue)
.replace()
有几种不同的模式,具体取决于您为其参数提供的值:
searchValue
是......- 没有
/g
的正则表达式:替换第一次出现。 - 带
/g
的正则表达式:替换所有出现的事件。 - 字符串:替换第一次出现(字符串逐字解释,而不是正则表达式)。唉,这意味着字符串作为搜索值的用途有限。在本章的后面,你会发现是一个工具函数,用于将任意文本转换为正则表达式。
- 没有
replacementValue
是......- 字符串:描述替换
- 功能:计算替换
下一小节假设正在使用带有/g
的正则表达式。
37.5.5.1. replacementValue
是一个字符串
如果替换值是字符串,则美元符号具有特殊含义 - 它插入与正则表达式匹配的内容:
文本 | 结果 |
---|---|
$$ |
单个$ |
$& |
完整匹配项 |
`$`` | 匹配项前的文字 |
$' |
匹配项后的文字 |
$n |
位置捕获组n (n > 0 ) |
$<name> |
命名捕获组name |
- 示例:在匹配的子字符串之前,之内和之后插入文本。
> 'a1 a2'.replace(/a/g, "($`|$&|$')") '(|a|1 a2)1 (a1 |a|2)2'
- 示例:插入位置捕获组。
> const regExp = /^([A-Za-z]+): (.*)$/ug; > 'first: Jane'.replace(regExp, 'KEY: $1, VALUE: $2') 'KEY: first, VALUE: Jane'
- 示例:插入命名捕获组。
> const regExp = /^(?<key>[A-Za-z]+): (?<value>.*)$/ug; > 'first: Jane'.replace(regExp, 'KEY: $<key>, VALUE: $<value>') 'KEY: first, VALUE: Jane'
37.5.5.2. replacementValue
是一个功能
- 如果替换值是函数,则可以计算每个替换值。在下面的示例中,我们将我们找到的每个非负整数乘以 2。
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
37.5.6. 使用正则表达式的其他方法
String.prototype.split()
的第一个参数是字符串或正则表达式。如果是后者,则将组捕获的子串添加到方法的结果中:> 'a : b : c'.split(/( *):( *)/) [ 'a', ' ', ' ', 'b', ' ', ' ', 'c' ]
有关更多信息,请参阅 16.字符串 章节。
37.6. 标志/g
及其陷阱
如果打开/g
,以下两个正则表达式方法会执行异常操作:
RegExp.prototype.exec()
RegExp.prototype.test()
然后可以重复调用它们并在字符串中传递所有匹配项。正则表达式的属性.lastIndex
用于跟踪字符串中的当前位置。例如:
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
怎么会有问题呢?我们将首先探讨问题然后解决问题。
37.6.1. 问题:您无法使用标志/g
内联正则表达式
- 无法内联带有
/g
的正则表达式:例如,在以下while
循环中,每次检查条件时都会创建正则表达式。因此,它的.lastIndex
始终为零,循环永远不会终止。let count = 0; // Infinite loop while (/a/g.test('babaa')) { count++; }
37.6.2. 问题:删除/g
可能会破坏代码
- 如果代码需要带有
/g
的正则表达式并且在.exec()
或.test()
的结果上有一个循环,那么没有/g
的正则表达式会导致无限循环:const regExp = /a/; // Missing: flag /g let count = 0; // Infinite loop while (regExp.test('babaa')) { count++; }
- 为什么?因为
.test()
总是返回第一个结果,true
,而不是false
。
37.6.3. 问题:添加/g
可能会破坏代码
- 使用
.test()
时,还有另一个警告:如果要检查正则表达式是否与字符串匹配,则正则表达式必须不具有/g
。否则,每次调用.test()
时,通常会得到不同的结果:> const r = /^X/g; > r.test('Xa') true > r.test('Xa') false
- 通常,如果您打算以这种方式使用
.test()
,则不会添加/g
。但是,例如,如果您使用相同的正则表达式进行测试和替换,则会发生这种情况。或者,如果您通过参数获得正则表达式。
37.6.4. 问题:如果.lastIndex
不为零,代码可能会中断
- 创建正则表达式时,
.lastIndex
初始化为零。如果代码曾经收到.lastIndex
不为零的正则表达式,它可能会中断。例如:const regExp = /a/g; regExp.lastIndex = 4; let count = 0; while (regExp.test('babaa')) { count++; } assert.equal(count, 1); // should be 3
- 如果正则表达式被共享且未正确处理,则
.lastIndex
不为零可能相对容易发生。
37.6.5. 处理/g
和.lastIndex
请考虑以下情形:您想要实现一个函数countOccurrences(regExp, str)
,它计算regExp
在str
内的匹配频率。你如何防止错误的regExp
破坏你的代码?我们来看看三种方法。
- 首先,如果未设置
/g
或.lastIndex
不为零,则可以抛出异常: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
不会改变。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
。function countOccurrences(regExp, str) { if (!regExp.global) { throw new Error('Flag /g of regExp must be set'); } return (str.match(regExp) || []).length; }
37.7. 使用正则表达式的技巧
37.7.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
禁止许多逃逸:其中包括\:
和\-
。 - 这是您可以使用
escapeForRegExp()
多次替换任意文本的方法:> const re = new RegExp(escapeForRegExp(':-)'), 'ug'); > ':-) :-) :-)'.replace(re, '🙂') '🙂 🙂 🙂'
37.7.2. 匹配一切或什么也没有
有时,您可能需要一个匹配所有内容的正则表达式。例如,作为标记值。
- 匹配所有内容:
/(?:)/
(空组匹配所有内容;使其无法捕获,避免不必要的工作)> /(?:)/.test('') true > /(?:)/.test('abc') true
- 什么都不匹配:
/.^/
(一旦匹配进展超出第一个字符,^
就不再匹配了)> /.^/.test('') false > /.^/.test('abc') false
下一节:本章介绍 JavaScript 用于处理日期的 API - Date。