📏现代 JavaScript —— 正则表达式
00 分钟
2022-9-16
2022-9-15
type
status
date
slug
summary
tags
category
icon
password
Edited
Sep 15, 2022 01:43 PM
Created
Sep 15, 2022 01:00 PM

方法

str.match(regexp)

str.match(regexp) 方法在字符串 str 中寻找 regexp 的所有匹配项。
它有 3 种工作模式:
  1. 如果正则表达式具有修饰符 g,它返回一个由所有匹配项所构成的数组
  1. 如果没有这样的修饰符,它则会以数组形式返回第一个匹配项,索引 0 处保存着完整的匹配项,返回的结果的属性中还有一些其他详细信息
  1. 如果没有匹配项,则返回 null(无论是否有修饰符 g)。

str.matchAll(regexp)

当我们搜索所有匹配项(修饰符 g)时,match 方法不会返回组的内容。
就像 match 一样,它寻找匹配项,但有 3 个区别:
  1. 它返回的不是数组,而是一个可迭代的对象。
  1. 当存在修饰符 g 时,它将每个匹配项以包含组的数组的形式返回。
  1. 如果没有匹配项,则返回的不是 null,而是一个空的可迭代对象。
matchAll 返回的每个匹配项,与不带修饰符 g 的 match 所返回的格式相同:具有额外 index(字符串中的匹配索引)属性和 input(源字符串)的数组:

str.split(regexp|substr, limit)

使用正则表达式(或子字符串)作为分隔符来分割字符串。
重要限制:search 仅查找第一个匹配项。

str.search(regexp)

方法 str.search(regexp) 返回第一个匹配项的位置,如果没找到,则返回 -1

str.replace(str|regexp, str|func)

这是用于搜索和替换的通用方法,是最有用的方法之一。它是搜索和替换字符串的瑞士军刀。
str.replace(regexp, replacement) 方法使用 replacement 替换在字符串 str 中找到的 regexp 的匹配项(如果带有修饰符 g 则替换所有匹配项,否则只替换第一个)。
第二个参数是字符串 replacement。我们可以在其中使用特殊的字符组合来对匹配项进行插入:
符号
替换字符串中的行为
$&
插入整个匹配项
$`
插入字符串中匹配项之前的字符串部分
$'
插入字符串中匹配项之后的字符串部分
$n
如果 n 是一个 1-2 位的数字,则插入第 n 个分组的内容,详见 捕获组
$<name>
插入带有给定 name 的括号内的内容,详见 捕获组
$$
插入字符 $
对于需要“智能”替换的场景,第二个参数可以是一个函数。
每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。
该函数 func(match, p1, p2, ..., pn, offset, input, groups) 带参数调用:
  1. match —— 匹配项,
  1. p1, p2, ..., pn —— 捕获组的内容(如有),
  1. offset —— 匹配项的位置,
  1. input —— 源字符串,
  1. groups —— 具有命名的捕获组的对象。
如果正则表达式中没有括号,则只有 3 个参数:func(str, offset, input)

str.replaceAll(str|regexp, str|func)

这个方法与 str.replace 本质上是一样的,但有两个主要的区别:
  1. 如果第一个参数是一个字符串,它会替换 所有出现的 和第一个参数相同的字符串,而 replace 只会替换 第一个
  1. 如果第一个参数是一个没有修饰符 g 的正则表达式,则会报错。带有修饰符 g,它的工作方式与 replace 相同。
replaceAll 的主要用途是替换所有出现的字符串。

regexp.exec(str)

regexp.exec(str) 方法返回字符串 str 中的 regexp 匹配项。与以前的方法不同,它是在正则表达式而不是在字符串上调用的。
它的行为取决于正则表达式是否具有修饰符 g
如果没有修饰符 g,则 regexp.exec(str) 会返回与 第一个匹配项,就像 str.match(regexp) 那样。这种行为并没有带来任何新的东西。
但是,如果有修饰符 g,那么:
  • 调用 regexp.exec(str) 会返回第一个匹配项,并将紧随其后的位置保存在属性 regexp.lastIndex 中。
  • 下一次这样的调用会从位置 regexp.lastIndex 开始搜索,返回下一个匹配项,并将其后的位置保存在 regexp.lastIndex 中。
  • ……以此类推。
  • 如果没有匹配项,则 regexp.exec 返回 null,并将 regexp.lastIndex 重置为 0
因此,重复调用会一个接一个地返回所有匹配项,使用属性 regexp.lastIndex 来跟踪当前搜索位置。
我们可以通过手动设置 lastIndex,用 regexp.exec 从给定位置进行搜索。

regexp.test(str)

方法 regexp.test(str) 查找匹配项,然后返回 true/false 表示是否存在。
如果正则表达式带有修饰符 g,则 regexp.test 从 regexp.lastIndex 属性开始查找并更新此属性,就像 regexp.exec 一样。
因此,我们可以用它从给定位置进行查找
 

模式(Patterns)和修饰符(flags)

在 JavaScript 中,我们可以通过 RegExp 对象使用它们,也可以与字符串方法结合使用。
正则表达式(可叫作 “regexp”,或 “reg”)包括 模式 和可选的 修饰符
有两种创建正则表达式对象的语法:
斜线 /.../ 告诉 JavaScript 我们正在创建一个正则表达式。它的作用与字符串引号的作用相同。
这两种语法之间的主要区别在于,使用斜线 /.../ 的模式不允许插入表达式(如带有 ${...} 的字符串模板)。它是完全静态的。

修饰符

在 JavaScript 中,有 6 个修饰符:
  • i
    • 使用此修饰符后,搜索时不区分大小写:A 和 a 之间没有区别(请参见下面的示例)。
  • g
    • 使用此修饰符后,搜索时会寻找所有的匹配项 —— 没有它,则仅返回第一个匹配项。
  • m
    • 多行模式(详见 锚点 ^ $ 的多行模式,修饰符 "m")。
  • s
    • 启用 “dotall” 模式,允许点 . 匹配换行符 \n(在 字符类 中有详细介绍)。
  • u
    • 开启完整的 Unicode 支持。该修饰符能够正确处理代理对。详见 Unicode:修饰符 "u" 和类 \p{...}。
  • y
    • 粘滞模式,在文本中的确切位置搜索(详见 粘性修饰符 "y",在位置处搜索)
 

字符类

字符类(Character classes) 是一种特殊的符号,匹配特定集合中的任何符号。
最常用的是:
\d(“d” 来自 “digit”)
数字:从 0 到 9 的字符。
\s(“s” 来自 “space”)
空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v\f 和 \r
\w(“w” 来自 “word”)
“单字”字符:拉丁字母或数字或下划线 _。非拉丁字母(如西里尔字母或印地文)不属于 \w
例如,\d\s\w 表示“数字”,后跟“空格字符”,后跟“单字字符”,例如 1 a
正则表达式可能同时包含常规符号和字符类。
示例:"+7(903)-123-45-67",我们需要将其转换为纯数字:79031234567

反向类

对于每个字符类,都有一个“反向类”,用相同的字母表示,但是大写的。
“反向”表示它与所有其他字符匹配
\D
非数字:除 \d 以外的任何字符,例如字母。
\S
非空格符号:除 \s 以外的任何字符,例如字母。
\W
非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格。

点(.)匹配“任何字符”

点 . 是一种特殊字符类,它与“除换行符之外的任何字符”匹配。
带有修饰符 “s” 时点字符类匹配任何字符
 

Unicode:修饰符 "u" 和类 \p{...}

修饰符 "u"

默认情况下,正则表达式也会把一个 4 个字节的“长字符”当成一对 2 个字节长的字符。正如在字符串中遇到的情况,这将导致一些奇怪的结果。
与字符串有所不同的是,正则表达式有一个修饰符 u 被用以解决此类问题。当一个正则表达式带有这个修饰符后,4 个字节长的字符将被正确地处理。同时也能够使用 Unicode 属性进行查找了

\p{...}

Unicode 中的每个字符都有很多属性。它们描述了字符所属的“类别”,包含了关于字符的各种信息。
例如,如果一个字符具有 Letter 属性,这意味着这个字符归属于(任意语言的)字母表。而 Number 属性则表示这是一个数字:也许是阿拉伯数字,亦或是中文数字,等等。
我们可以查找具有某种属性的字符,写作 \p{…}。为了使用 \p{…},一个正则表达式必须使用修饰符 u
举个例子,\p{Letter} 表示任何语言中的一个字母。我们也可以使用 \p{L}
以下是主要的字符类别和它们对应的子类别:
  • 字母(Letter)L
    • 小写(lowercase)Ll
    • 修饰(modifier)Lm
    • 首字母大写(titlecase)Lt
    • 大写(uppercase)Lu
    • 其它(other)Lo
  • 数字(Number)N
    • 十进制数字(decimal digit)Nd
    • 字母数字(letter number)Nl
    • 其它(other)No
  • 标点符号(Punctuation)P
    • 连接符(connector)Pc
    • 横杠(dash)Pd
    • 起始引号(initial quote)Pi
    • 结束引号(final quote)Pf
    • 开(open)Ps
    • 闭(close)Pe
    • 其它(other)Po
  • 标记(Mark)M(accents etc):
    • 间隔合并(spacing combining)Mc
    • 封闭(enclosing)Me
    • 非间隔(non-spacing)Mn
  • 符号(Symbol)S
    • 货币(currency)Sc
    • 修饰(modifier)Sk
    • 数学(math)Sm
    • 其它(other)So
  • 分隔符(Separator)Z
    • 行(line)Zl
    • 段落(paragraph)Zp
    • 空格(space)Zs
  • 其它(Other)C
    • 控制符(control)Cc
    • 格式(format)Cf
    • 未分配(not assigned)Cn
    • 私有(private use)Co
    • 代理伪字符(surrogate)Cs
 

锚点:字符串开始 ^ 和末尾 $

插入符号 ^ 和美元符号 $ 在正则表达式中具有特殊的含义。它们被称为“锚点”。
插入符号 ^ 匹配文本开头,而美元符号 $ 则匹配文本末尾。

锚点 ^ $ 的多行模式,修饰符 "m"

多行模式由修饰符 m 启用。
它只影响 ^ 和 $ 的行为。
在多行模式下,它们不仅仅匹配文本的开始与末尾,还匹配每一行的开始与末尾。
⚠️
“行的开头”表示“就在换行符之后”:多行模式下的测试 ^ 匹配所有以换行符 \n 开头的位置。
 

词边界:\b

词边界 \b 是一种检查,就像 ^ 和 $ 一样。
当正则表达式引擎(实现正则表达式搜索的程序模块)遇到 \b 时,它会检查字符串中的位置是否是词边界。
有三种不同的位置可作为词边界:
  • 在字符串开头,如果第一个字符是单词字符 \w
  • 在字符串中的两个字符之间,其中一个是单词字符 \w,另一个不是。
  • 在字符串末尾,如果最后一个字符是单词字符 \w
位置对应于 \b
位置对应于 \b
\b 既可以用于单词,也可以用于数字。
 

转义,特殊字符

正如我们所看到的,反斜杠 \ 用于表示字符类,例如 \d。所以它是正则表达式中的一个特殊字符(就像在常规字符串中一样)。

转义

假如我们想要找到一个点号 .
要将特殊字符用作常规字符,请在其前面加上反斜杠:\.
这就是所谓的“转义字符”。

一个斜杠

一个斜杠符号 '/' 并不是一个特殊字符,但是它被用于在 Javascript 中开启和关闭正则匹配:/...pattern.../
如果我们使用 new RegExp 创建正则表达式,那么我们不必转义 /,但需要进行一些其他转义。
如果要修复这个问题,我们需要双斜杠,因为引用会把 \\ 变为 \

Summary

  • 要在字面意义上搜索特殊字符 [ \ ^ $ . | ? * + ( ),我们需要在它们前面加上一个反斜杠 \(“转义它们”)。
  • 如果在 /.../ 内(但不在 new RegExp 内),我们还需要转义 /
  • 当将字符串传递给给 new RegExp 时,我们需要双反斜杠 \\,因为字符串引号会消耗一个反斜杠。
 

集合和范围 [...]

在方括号 […] 中的几个字符或者字符类表示“搜索给定字符中的任意一个”。

集合

例如,[eao] 表示以下 3 个字符中的任何一个:'a''e' 或 'o' 。
这就是所谓的 集合。在正则表达式中,可以将集合和常规字符一起使用。

范围

方括号也可以包含 字符范围
例如,[a-z] 表示从 a 到 z 范围内的字符,[0-5] 表示从 0 到 5 的数字。
[0-9A-F] 中有两个范围:它搜索一个字符,该字符要么是在 0 到 9 范围内的数字,要么是从 A 到 F 的字母。
如果我们还想查找小写字母,则可以添加范围 a-f[0-9A-Fa-f]。或添加标志 i
也可以组合多个类,例如 [\s\d] 表示“空格字符或数字”。
ℹ️
字符类是某些字符集合的简写
  • \d —— 和 [0-9] 相同,
  • \w —— 和 [a-zA-Z0-9_] 相同,
  • \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 Unicode 空格字符相同。

排除范围

除了普通的范围匹配,还有像这样 [^…] 的“排除”范围匹配。
通过在开头添加插入符号 ^ 来表示匹配所有 除了给定的字符 之外的任意字符。
  • [^aeyo] —— 匹配除了 'a''e''y' 或 'o' 之外的任何字符。
  • [^0-9] —— 匹配除了数字之外的任何字符,与 \D 作用相同。
  • [^\s] —— 匹配任何非空格字符,与 \S 作用相同。
如果集合中有代理对(surrogate pairs),则需要标志 u 才能使它们正常工作。
 

量词 +, *, ? 和 {n}

数量 {n}

最简单的量词便是大括号中的数字:{n}
在一个字符(或一个字符类,或 [...] 等)后附加一个量词,用来指出我们具体需要的数量。
确切的位数:{5}
\d{5} 表示 5 位数,与 \d\d\d\d\d 相同。
范围:{3,5},匹配 3-5 个
可以省略上限。那么正则表达式 \d{3,} 就会查找位数大于等于 3 的数字

缩写

大多数常用的量词都有简写形式:
+ 代表“一个或多个”,与 {1,} 相同。
? 代表“零个或一个”,与 {0,1} 相同。
* 代表“零个及以上”,与 {0,} 相同。
“打开或关闭的不带特性的 HTML 标签”的正则表达式:/<\/?[a-z][a-z0-9]*>/i
 

贪婪量词和惰性量词

为了查找到一个匹配项,正则表达式引擎采用了以下算法:
  • 对于字符串中的每一个位置
    • 尝试匹配该位置的模式。
    • 如果未匹配,则转到下一个位置。

工作方式

 
在贪婪模式下(默认情况),量词都会尽可能多地重复。
正则表达式引擎尝试用 .+ 去匹配尽可能多的字符,然后在模式的其余部分不匹配时再将其逐一缩短。

惰性模式

惰性模式中的量词与贪婪模式中的是相反的。它表示:“重复最少的次数”。
我们可以通过在量词后面添加一个问号 '?' 来启用它,这样匹配模式就变成了 *? 或 +?,甚至将 '?' 变成 ??
这么说吧:通常问号 ? 本身就是一个量词(0 或 1),但如果将其放到 另一个量词(甚至是它自己)后面,就会有不同的含义 —— 它将匹配的模式从贪婪转为惰性。

工作方式

 
惰性模式仅对带有 ? 的量词启用
其它量词依旧保持贪婪模式。

替代方法

使用正则表达式,通常有不止一种方式可以做相同的事。
在例子中,我们可以在不启用惰性模式的情况下使用正则表达式 "[^"]+" 找到带引号的字符串:
 

捕获组

模式的一部分可以用括号括起来 (...)。这被称为“捕获组(capturing group)”。
这有两个影响:
  1. 它允许将匹配的一部分作为结果数组中的单独项。
  1. 如果我们将量词放在括号后,则它将括号视为一个整体。
 
不带括号,模式 go+ 表示 g 字符,其后 o 重复一次或多次。例如 goooo 或 gooooooooo
括号将字符组合,所以 (go)+ 匹配 gogogogogogo等。

匹配中的括号的内容

括号被从左到右编号。正则引擎会记住它们各自匹配的内容,并允许在结果中获取它。
方法 str.match(regexp),如果 regexp 没有修饰符 g,将查找第一个匹配项,并将它作为数组返回:
  1. 在索引 0 处:完整的匹配项。
  1. 在索引 1 处:第一个括号的内容。
  1. 在索引 2 处:第二个括号的内容。
  1. ……等等……

嵌套组

括号可以嵌套。在这种情况下,编号也从左到右。
例如,在搜索标签 <span class="my"> 时,我们可能会对以下内容感兴趣:
  1. 整个标签的内容:span class="my"
  1. 标签名称:span
  1. 标签特性:class="my"
让我们为它们添加括号:<(([a-z]+)\s*([^>]*))>
这是它们的编号方式(根据左括号从左到右):
notion image

可选组

即使组是可选的并且在匹配项中不存在(例如,具有量词 (...)?),也存在相应的 result 数组项,并且等于 undefined
例如,让我们考虑正则表达式 a(z)?(c)?。它查找 "a",后面是可选的 "z",然后是可选的 "c"
如果我们在单个字母的字符串上运行 a,则结果为:

命名组

用数字记录组很困难。对于简单的模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号命名。
在左括号后紧跟着放置 ?<name> 即可完成对括号的命名。
匹配的组在 .groups 属性中。

替换中的捕获组

让我们能够替换 str 中 regexp 的所有匹配项的方法 str.replace(regexp, replacement) 允许我们在 replacement 字符串中使用括号中的内容。这使用 $n 来完成,其中 n 是组号。
对于命名的括号,引用为 $<name>

非捕获组 ?:

有时我们需要用括号才能正确应用量词,但我们不希望它们的内容出现在结果中。
可以通过在开头添加 ?: 来排除组。
例如,如果我们要查找 (go)+,但不希望括号内容(go)作为一个单独的数组项,则可以编写:(?:go)+
 

模式中的反向引用:\N 和 \k<name>

我们不仅可以在结果或替换字符串中使用捕获组 (...) 的内容,还可以在模式本身中使用它们。

按编号反向引用:\N

可以使用 \N 在模式中引用一个组,其中 N 是组号。
看一个示例:
['"](.*?)['"],但它会找到带有混合引号的字符串,例如 "...' 和 '..."。当一种引号出现在另一种引号内,比如在字符串 "She's the one!" 中时,便会导致不正确的匹配
该模式找到了一个开头的引号 ",然后文本被匹配,直到另一个引号 ',该匹配结束。
为了确保模式查找的结束引号与开始的引号完全相同,我们可以将其包装到捕获组中并对其进行反向引用:(['"])(.*?)\1
正则表达式引擎会找到第一个引号 (['"]) 并记住其内容。那是第一个捕获组。
在模式中 \1 表示“找到与第一组相同的文本”,在我们的示例中为完全相同的引号。
与此类似,\2 表示第二组的内容,\3 —— 第三分组,依此类推。
ℹ️
如果我们在捕获组中使用 ?:,那么我们将无法引用它

按命名反向引用:\k<name>

如果一个正则表达式中有很多括号,给它们起个名字会便于引用。
要引用命名的捕获组,我们可以使用:\k<name>
 

选择 (OR) |

选择是正则表达式中的一个术语,实际上是一个简单的“或”。
在正则表达式中,它用竖线 | 表示。
例如,我们想要找出编程语言:HTML、PHP、Java 或 JavaScript。
对应的正则表达式为:html|php|java(script)?
方括号只允许字符或字符类。选择允许任何表达式。正则表达式 A|B|C 表示表达式 AB 或 C 其一均可。
用于时间匹配的正则表达式
 

前瞻断言与后瞻断言

有时我们只需要为一个模式找到那些在另一个模式之后或之前的匹配项。
有一种特殊的语法,称为“前瞻断言(lookahead)”和“后瞻断言(lookbehind)”。

前瞻断言

语法为:x(?=y),它表示“仅在后面是 Y 时匹配 X”。There may be any pattern instead of X and Y.
注意:前瞻断言只是一个测试,括号 (?=...) 中的内容不包含在匹配结果 30 中。
例如,\d+(?=\s)(?=.*30) 查找后跟着空格 (?=\s) 的 \d+,并且有 30 在它之后的某个地方 (?=.*30)

否定的前瞻断言

假设我们想要一个数量,而不是来自同一字符串的价格。那是一个数字 \d+,后面不是 
语法是:X(?!Y),意思是“搜索 X,但前提是后面没有 Y”。

后瞻断言

⚠️
非 V8 引擎的浏览器不支持后瞻断言
前瞻断言允许添加一个“后面要跟着什么”的条件判断。
后瞻断言也类似,只不过它是在相反的方向上进行条件判断。也就是说,它只允许匹配前面有特定字符串的模式。
语法为如下:
  • 肯定的后瞻断言:(?<=Y)X,匹配 X,仅在前面是 Y 的情况下。
  • 否定的后瞻断言:(?<!Y)X,匹配 X,仅在前面不是 Y 的情况下。

捕获断言

 

灾难性回溯

 

粘性修饰符 "y",在位置处搜索

y 修饰符让我们能够在源字符串中的指定位置进行搜索。
 
regexp.exec(str)
对于没有修饰符 g 和 y 的 regexp,此方法仅查找第一个匹配项,就像 str.match(regexp) 一样。
……但是如果有修饰符 g,那么它就会从存储在 regexp.lastIndex 属性中的位置开始在字符串 str 中进行搜索。如果找到匹配项,则将在匹配后立即将 regexp.lastIndex 设置为索引。
我们可以手动设置 lastIndex 
因此,连续调用 regexp.exec(str) 会一个接一个地返回匹配。
 
修饰符 y 使 regexp.exec 精确搜索位置 lastIndex,而不是“从”它开始。
 

参考链接:
  1. 正则表达式 (javascript.info)
 
上一篇
Fastify 的使用
下一篇
现代 JavaScript —— 杂项(Web Components)

评论
Loading...