JavaScript中的正则表达式

647 阅读12分钟

正则表达式

Regular expressions are patterns used to match character combinations in strings.

In JavaScript, regular expressions are also objects. These patterns are used with the exec() and test() methods of RegExp, and with the match(), matchAll(), replace(), replaceAll(), search(), and split() methods of String

Regular expressions - JavaScript | MDN (mozilla.org)

两种创建形式

基本形式(字面量)

正则表达式内容用一对斜杠包裹

/abc/ // 匹配 abc 子串

使用构造函数创建正则表达式

let re = new RegExp('abc')

脚本加载后正则表达式字面量就会被编译,当正则表达式不变时,使用字面量可获得更好的性能;在脚本运行过程中编译用构造函数创建的正则表达式,如果正则表达式会动态变化,那么就需要使用构造函数来创建正则表达式

正则表达式中的自变量

使用模板字符串的变量拼接将变量代入

let a = 'abc'
`/${a}/` // 此时匹配的是 abc 

正则表达式的模式匹配

简单的模式(字面值)

由基本字符组成,这些字符直接匹配字符内容(相同的字符及顺序)

/abc/ // 匹配 abc 子串,不能有任何差异

特殊字符

利用特殊字符可以构造一些特别的模式匹配,而不仅仅只是匹配固定的基本字符内容,比如匹配不定个数的b/b*/)、以a开头的字符串/^a/

反斜杠\

在非特殊字符之前的反斜杠表示下一个字符是特殊字符,此时该非特殊字符不再匹配字面上的对应字符,比如/\d/,匹配 0~9

要注意在字符串字面量中\是转义字符,所以为了在模式字符串中添加一个反斜杠,需要在字符串中对反斜杠进行转义,也就是new RegExp("\\b")

还有一种特殊情况是当需要匹配反斜杠时,在字符串字面量和正则表达式字面量中都需要对反斜杠进行转义,比如/\b\\/"\b\\\\"

字符类(Character classes)

利用字符类可以指定字符的类型(如数字、字母、控制字符)

小数点.

  • 默认匹配除行终止符(\n, \r, \u2028 or \u2029)以外的任何单个字符
  • 在一个字符类中,这个点会失去他的特殊含义,并且仅仅只匹配字面上的.
/.n/ 
// 将会匹配 "nay, an apple is on the tree" 中的 'an' 和 'on',但是不会匹配 'nay'

在 ES2018 中如果 s "dotAll" 标志位被设为 true,它也会匹配换行符(ES9的正则扩展)

\d

匹配任何为阿拉伯数字的单个字符

\D

匹配任何为非阿拉伯数字的单个字符

\w

匹配任何为大小写字母、数字和下划线的单个字符

\W

匹配任何为非大小写字母、非数字和非下划线的单个字符

\s

匹配任何为空白字符的单个字符,包括空格、制表符、换页符、换行符和其他的Unicode空格

\S

匹配任何为非空白字符的单个字符

匹配转义字符

\t

匹配一个水平制表符

\v

匹配一个垂直制表符

\r

匹配一个回车符(carriage return)

\n

匹配一个换行符(line-feed)

\f

匹配一个换页符(form-feed)

[\b]

匹配一个退格符(注意和\b的区别)

\0

匹配一个空字符

\cX

匹配一个控制符(使用 caret notation,比如^A~^Z)。X 表示 A-Z

匹配特定数值

\xhh

匹配两个十六机制数(hh),注意这里的x不是指匹配项目,而是切实的x

\uhhhh

匹配一个UTF-16字符单元(hhhh

\u{hhhh} or \u{hhhhh}

(当设置了 u 标志位)匹配Unicode字符值(十六进制数)

\p{UnicodeProperty}, \P{UnicodeProperty}

通过字符的Unicode character properties匹配一个字符(如 emoji 字符、日本片假名字符或者中文汉字)

数量修饰符(Quantifiers)

用于指定应该匹配的字符或表达式的数量

注:以下的X表示的是正则表达式项,它不仅表示单个字符,还表示 character classes, Unicode property escapes, groups and ranges

X*

匹配X0次或多次

X+

匹配X1次或多次

X?

  • 当不跟在任何数量修饰符之后,则表示匹配X0次或1次
  • 当紧邻在数量修饰符之后(* + ? {}),将会使得数量修饰符变成非贪婪模式(non-greedy),也就是匹配尽可能少的字符。默认情况下数量修饰符会匹配尽可能多的字符,也就是贪婪模式(greedy

X{n}

n是一个正整数,匹配X正好n

X{n,}

n是一个正整数,匹配X至少n

X{n,m}

n是0或正整数,m是一个正整数,并且m > n,匹配X至少n次,至多m

贪婪和非贪婪模式(Greedy versus non-greedy)

默认情况下所有的数量修饰符都是处于贪婪模式下,这意味着它们将尝试匹配尽可能多的字符。在数量修饰符后方添加问号字符?会使得数量修饰符进入非贪婪模式,这意味着一旦找到了匹配的内容就会停止匹配(匹配尽可能少的字符)

"some <foo> <bar> new </bar> </foo> thing";
/<.*>/; // will match "<foo> <bar> new </bar> </foo>"
/<.*?>/; // will match "<foo>"
// 例子来自 MDN 文档

分组和范围(Groups and ranges)

x|y

匹配xy

[xyz], [a-c]

这里定义了一个字符类,将匹配一个其中包含字符。可以利用破折号-来简略指定字符范围比如[abcd] => [a-d],如果破折号为其中的第一个或最后一个字符,则破则号仅仅表示字面上的破则号

[^xyz], [^a-c]

定义了一个否定字符类(可以看作[xyz], [a-c]的补集),将匹配一个任何其中不包含的字符

(x)

捕获分组(Capturing group),将会匹配x并且将匹配到的内容记录下来,一个表达式中的捕获分组可能有多个,这些内容被存储在一个数组中,元素的顺序和左括号的顺序相同。可以通过下标([1], ..., [n])访问,或者通过正则表达式对象的属性访问($1, ..., $n

捕获分组是一定会将捕获到的子串记录下来以便于重新调用的。如果不想记录应该使用非捕获分组

(?:x)

非捕获分组(Non-capturing group),将会匹配x但不会将内容记录,所以匹配到的内容不能被再次调用

\num

num是一个正整数,\num是对第 n 个左括号对应分组匹配的最后一个子串的反向引用(对应了左括号的顺序)。比如\(foo) (bar) \1 \2\对应的匹配内容就是foo bar foo bar

(?<name>x)

具名捕获分组,name就是其对应的名称(尖括号<>不能省略),x是要匹配的内容。具名捕获分组匹配到的内容将被存储在一个groups属性中,通过matches.groups.name方式获取匹配到的子串(matches 为对应的匹配内容返回结果)

\k<name>

是对具名捕获分组匹配到的最后一个子串的反向引用,name为对应的具名捕获分组名称(尖括号<>不能省略),k为字面上的k,这里用于标识这一引用,不能省略

断言(Assertions)

断言包括对边界断言、对字符序列的断言(说白了就是按照某种条件判断这一子串是否符合要求)

脱字符^

匹配输入的开始(开头字符)。如果多行标志被设置为 true,那么也匹配换行符后紧跟的位置

/^A/
// 不匹配 an A 中的 A ,匹配 An E 中的 E

dollar符$

匹配输入的结束(末尾字符)。如果多行标志被设置为 true,那么也匹配换行符前的位置

/t$/ 
// 不匹配 eater 中的 t,但是会匹配 eat 中的 t

\b

匹配一个单词的边界。这个边界位置是指一个单词的字符不被另一个单词字符跟随,或者没有紧跟着另一个单词字符的位置

"moon";
/\bm/; // 匹配 m
/oon\b/; // 匹配 oon
/oo\b/; // 不匹配任何内容
/\w\b\w/; // 不可能匹配任何东西,因为一个单词的字符不可能跟着一个单词边界又跟着另一个单词的字符

\B

匹配非单词边界。这种位置是指所处位置的前一个和后一个字符是同一类型的:要么都是单词字符,要么都是非单词字符,比如两个字母之间或两个空格之间

字符串的起始位置和结束位置就被认为是非单词位置

"noon";
/\Bon/; // on
/no\B/; // no

x(?=y)

先行断言(Lookahead assertion),仅仅当 'x' 后面跟着 'y' 时匹配 ' x'

/Jack(?=Sprat)/ // 会匹配到'Jack'仅当它后面跟着'Sprat'
// 但是‘Sprat’不是匹配结果的一部分

x(?!y)

否定先行断言(Negative lookahead assertion,正向否定查找),仅仅当 'x' 后面不跟着 'y' 时匹配 'x'

(?<=y)x

后行断言(Lookbehind assertion),仅当 'x' 前面是 'y' 时匹配 'x'

(?<!y)x

否定后行断言(Negative lookbehind assertion,反向否定查找)仅仅当 'x' 前面不是 'y' 时匹配 'x'

Unicode 属性转义(Unicode property escapes)

Unicode属性转义允许通过Unicode属性(Unicode properties)来匹配内容(emojis, letters等)

为了使Unicode属性转义生效,应该设置u标志位,表明一个字符串一定要被认为是一连串的Unicode符号点(Unicode code points)

语法

// Non-binary values
\p{UnicodePropertyValue}
\p{UnicodePropertyName=UnicodePropertyValue}

// Binary and non-binary values
\p{UnicodeBinaryPropertyName}

// Negation: \P is negated \p
\P{UnicodePropertyValue}
\P{UnicodeBinaryPropertyName}

Unicode property escapes - JavaScript | MDN (mozilla.org)

带标志的高级搜索

正则表达式有7个可选标志位,这些标志位可以单独使用也可以混合使用

请注意,标志是正则表达式的组成部分,一旦确定了正则表达式则无法通过后续操作添加或删除标志

在创建正则表达式字面量或对象时添加标志位

/pattern/flag;

const re = new RegExp("pattern", "flag");

以下位可选标志位及含义

FlagDescCorresponding prop
d为匹配子串生成开头和结尾的索引,索引通过 RegExpArray 的 indices 数组获得RegExp.prototype.hasIndices
g全局搜索。对于String相关方法意味着在整个字符串中搜索,对于RegExp相关方法意味着获得迭代进程,也就是多次调用相关方法会迭代的向前寻找字符串中匹配子串RegExp.prototype.global
i忽略大小写地进行搜索RegExp.prototype.ignoreCase
m多行搜索,这意味着在包含多个行的字符串中使用断言符号^$时,会匹配每行的开头和结尾,而不是默认的只匹配整个字符串的开头和结尾RegExp.prototype.multiline
s允许.匹配换行符RegExp.prototype.dotAll
u"unicode",意味着将模式视为一系列的unicode代码点RegExp.prototype.unicode
y进行粘性搜索,搜索的起始下标可以通过正则表达式对象的 lastIndex 属性自定义,如果 lastIndex 所标识的起始位置不是一个完整正则表达式匹配项的起始位置,那么将不会匹配到任何东西,并且 lastindex 归0;如果是一个完整正则表达式匹配项的起始位置,那么则会获得该匹配项,并更新 lastindex 为匹配项末尾下标加 1,并尝试继续匹配。这里的“粘性”想表达的是只有当匹配项是连续的、相“粘连”的,搜索才会从当前下标位置继续下去,否则 lastindex 归 0 ,从头开始。当 y 和 g 标志被同时使用时会省略 g 标志RegExp.prototype.sticky

以上所有标志位对应的对应RegExp的属性都是只读属性(布尔值)

这里需要注意的是,每次的匹配项搜索的起始位置都是由正则表达式对象的lastIndex属性决定的,这个值是允许用户设置的,用户可以通过设置lastIndex的值来自定义搜索的起始位置

在JavaScript中使用正则表达式

主要是在RegExp的方法和String的几个允许使用正则表达式参数的方法中使用

RegExp相关方法

RegExp.prototype.exec(str)

整个方法会执行一次在指定字符串中的匹配搜索,返回一个结果数组(RegExpArray,是一个伪数组,包含多个属性)或null

通过方法参数传入需要进行搜索匹配的字符串

当正则表达式的标志位包含gy时,正则表达式对象是有状态的。这个状态由正则表达式对象的属性lastIndex记录,这个属性表示的是每次匹配项搜索的开始索引,或者说上次搜索之后当前指向的下标位置。对于设置了g标志的正则表达式,接下来每执行一次exec(),就会改变一次lastIndex,这样每次搜索都会从新的位置开始搜索,直到字符串搜索结束(返回null),再调用exec()就会从头开始(lastIndex 归 0);而对于设置了y标志的正则表达式,按照粘性搜索的方式,只有当匹配项是连续出现的才会继续从当前位置往下搜索,否则从头开始(lastIndex 归 0)

这里要注意,当多次执行exec()时,如果中途目标字符串改变了,lastIndex也不会归 0 (除非返回 null),此时就会出错

RegExpArray

当执行一次exec()匹配成功时,将会返回一个RegExpArray,是一个包含额外属性的数组,当匹配成功一次就会更新一次RegExp对象的lastIndex属性;如果匹配失败则返回null,并且lastIndex置为 0

返回的RegExpArray将完整正则表达式的匹配项作为数组第一个元素[0],接下来的数组元素是各个捕获分组的匹配项(与捕获分组左括号出现的顺序一致)。数组包含的额外属性有:

  • input:输入的字符串
  • index:完整正则表达式匹配项在字符串中的起始索引
  • groups:记录具名捕获分组的对象,键为捕获分组名称,值为分组匹配项或undefined(无匹配项时),rea.groups.name
  • indices[][]:当设置了标志位d后,会记录匹配项的起始和终止索引,是一个二维数组,其中的每个一维数组存储一对起始和终止索引。其中也有一个groups属性,记录了具名捕获分组的匹配项起始和终止索引

RegExp.prototype.test(str)

该方法用于判断字符串中是否存在完整正则表达式的匹配项,返回布尔值

exec()类似,当正则表达式设置了标志位gy,当方法执行一次,就会搜索一次匹配项,此时RegExp对象的属性lastIndex也会更新,并且如果中途目标字符串换了,lastIndex也不会归 0 ,除非返回 false

String相关方法

String.prototype.match(regexp)

参数

根据传入的正则表达式参数来在字符串中检索匹配项,返回一个含有匹配项的数组或者null;如果不传入任何参数,将会获得一个含有一个空字符串的数组

当传入的参数不是一个RegExp对象类型,将会隐式地使用new RegExp()把它转化为RegExp对象,并且注意如果传入的是一个带符号的正数,其中的+会被省略掉,比如+10086最后形成的正则表达式为/10086/

返回值

当无匹配项时返回null,当有匹配项时返回的内容取决于是否设置了标志位g

如果设置了标志位g,则会返回所有和完整正则表达式的匹配项,并不包括捕获分组的匹配项

如果没有设置标志位g,则返回结果和执行RegExp.prototype.exec(str)不设置g时返回结果一致,也就是返回一个带有额外属性、包含第一个完整正则表达式的匹配项以及捕获分组匹配项的数组

String.prototype.matchAll(regexp)

返回一个含有所有匹配结果的迭代器,这是一个不能重新开始迭代过程的迭代器,迭代器每次迭代返回的数据是一个RegExpArray(和RegExp.prototype.exec()返回值类型相同)

该方法的特点

  • 参数为一个必须含有g标志位的正则表达式对象(如果为含有g标志位的正则表达式字符串,将会自动转化为RegExp对象),如果不含g将会抛出错误TypeError
  • 在没有matchAll()方法前,对于设置了g的正则表达式需要使用RegExp.prototype.exec()和循环语句来获取所有的匹配项及其具体信息(包括捕获分组匹配项、起始索引等信息),现在利用matchAll()方法返回的迭代器可以利用for...of...结构、Array.from()或扩展运算符...来简练地获取所有内容
  • matchAll()方法会在执行时内部创建一个传入的正则表达式对象的克隆,这使得原有的正则表达式对象的lastIndex参数不会被改变(这和exec()不一样,该方法会执行一次就更新一次lastIndex
  • matchAll()match()相比较的一个优点就是能够更好地获取捕获分组的信息。如上文所说match()中的正则表达式设置g标志位时只能获取到第一个匹配到的内容,那么只可以获取到第一个完整正则表达式匹配项的捕获分组相关信息,但使用matchAll()则可以通过返回的迭代器获取所有匹配项的捕获分组信息

String.prototype.search(regexp)

在字符串中执行一次匹配项搜索

找到完整正则表达式的匹配项则返回匹配项起始索引,否则返回 -1

String.prototype.replace(regexp, newSubstr|replacerFunction)

返回一个匹配项被替换后的新字符串,原字符串不会被改变

参数

这个方法的参数还可以是replace(substr, newSubstr|replacerFunction),此时第一个参数为一个字符串,并且只有目标字符串中第一个匹配substr的子串才会被替换

在这里主要讨论第一个参数为regexp的情况,此时第一个参数一定不能为字符串形式的正则表达式(此时不会被解析为正则表达式),必须为正则表达式对象或者字面量的正则表达式(比如只能为RegExp(/abc/)/abc/,但不能为"/abc/")。当正则表达式设置了标志位g则会替换所有完整正则表达式匹配项

第二个参数可以为一个用于替换完整正则表达式匹配项的字符串或者一个函数(生成用于替换完整正则表达式匹配项的字符串)

  • 当为用于替换的字符串时,可以为一个简单的字符串,也可以为一些特定的变量

    注意以下的变量都要用引号包裹起来

    PatternInserts
    $$插入一个$
    $&插入完整正则表达式匹配到的子串
    $`插入完整正则表达式匹配到的子串之前的字符串原有内容
    $'插入完整正则表达式匹配到的子串之后的字符串原有内容
    $nn是一个正整数且小于100,用于插入第 n 个捕获分组的匹配项,如果对应的内容不存在或第一个参数不为RegExp对象,则将被解析为字面量$n
    $<name>name表示的是具名捕获分组的名称,$<name>为对应捕获分组的匹配项。如果对应名称没有对应内容或者第一个参数不为RegExp,又或者这个名称并不存在,则将被解析为字面量$<name>
  • 当为一个函数时,这个函数会在匹配操作执行完之后执行,返回的是用于替换的字符串。如果第一个参数是设置了标志位g的RegExp对象,则该函数会被执行多次,以此替换多个完整正则表达式匹配项

    函数有以下参数:

    Possible nameSupplied value
    match完整正则表达式匹配到的子串,对应$&
    p1, p2, ...第 n 个捕获分组的匹配项,对应$n
    offset当前匹配项的起始索引,或者说起始位置在整个字符串中的位移
    string被进行匹配搜索的字符串,也就是调用该方法的字符串
    groups一个键为具名捕获分组名称、值为分组对应匹配项的对象

当设置第二个参数空字符串时,匹配项会被移除

String.prototype.replaceAll()

replaceAll()replace()的区别在于前者会替换所有的匹配项,并且要求如果第一个参数为RegExp,则必须设置标志位g,否则会抛出TypeError

String.prototype.split(separator?, limit?)

用于根据特定符号分割字符串,返回一个存有分割后子串的数组

参数

  • separator:可以为一个简单的字符串,也可以为一个regexp,如果此时regexp中有捕获分组,那么捕获分组的匹配项也会被记录在返回的数组中(紧跟在对应的完整正则表达式匹配项后面 )。如果参数为一个数组,则会被强制转换为一个字符串
  • limit:一个非负整数,限制方法返回的数组中子串的数量

返回值

如果字符串为空字符串但分隔符不是空字符串,那么就返回一个含有一个空字符串的数组; 如果字符串和分隔符都是空字符串,则返回一个空数组; 如果匹配不到分隔符或没有传入分隔符则返回含有一整个字符串的数组; 如果正常匹配则返回含有匹配子串的数组

关于使用split("")将字符串解析成字符数组的问题

此时是通过UTF-16的字符单元分割,这样会破坏代理对(surrogate pairs),如果字符串中有特殊字符可能会导致分割后字符为乱码。代理对被破坏是指部分字符它的字符编码并不是只对应了一个字符单元,而是两个(成一对,一个前导一个后缀),而通过这种方式的分割会使一个字符的两个字符单元变成分割两个独立字符放入数组,导致返回的结果出现乱码

所以如果想要将一个字符串变成一个字符数组,可以使用扩展运算符...str,数组方法Array.from(str),又或者使用str.split(/(?=[\s\S])/u)或str.split(/(?=.)/us)(实际用了先行断言x(?=y),此时x为空字符串""),又或者将字符通过循环一个一个的放入一个新的数组中

RexExp和RexExpArray中常用属性

当使用RexExp相关方法时会改变RexExp对象实例的相关属性

RexExp

  • source:对应的正则表达式
  • lastIndex:开始下一个匹配的下标位置(上一个匹配的结束下标加1)
  • hasIndices
  • global
  • dotAll
  • sticky
  • unicode
  • multiline
  • ignoreCase

RexExpArray

  • input:输入的字符串
  • index:完整正则表达式匹配项在字符串中的起始索引
  • groups:记录具名捕获分组的对象,键为捕获分组名称,值为分组匹配项或undefined(无匹配项时),rea.groups.name
  • indices[][]:当设置了标志位d后,会记录匹配项的起始和终止索引,是一个二维数组,其中的每个一维数组存储一对起始和终止索引。其中也有一个groups属性,记录了具名捕获分组的匹配项起始和终止索引

参考

Regular expressions - JavaScript | MDN (mozilla.org)