37. 正则表达式

37.1. 创建正则表达式

37.1.1. 字面义式 vs. 构造函数式

创建正则表达式的两种主要方法是:

  • 字面义式:/abc/ui,被静态编译(在加载时)。
  • 构造函数式:new RegExp('abc', 'ui'),被动态编译(在运行时)
    • 第二个参数是可选的。

两个正则表达式都有相同的两个部分:

  • 表达式体 abc - 实际正则表达式。
  • 标志 ui。标志配置了表达式模式如何被解释: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 %
  • .匹配任何字符。您可以使用标志/sdotall)来控制点.是否与行终止符匹配(下面详述)。
  • 字符转义(每个转义匹配一个固定字符):
    • 控制转义(用于一些控制字符):
      • \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 属性转义看起来像这样:

  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\pp相同。
  • 如果属性是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'
    

进一步阅读:

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):
    • 如果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):有匹配吗?

  • 如果regExpstr匹配,则正则表达式方法.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()将返回strregExp的第一个匹配的所有捕获:
    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' },
      }
    );
    
  • 如您所见,命名组keyvalue也作为位置组存在。
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 位置捕获组nn > 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),它计算regExpstr内的匹配频率。你如何防止错误的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。