一 概述
1. 正则表达式定义
正则表达式是用来匹配一串文本中符合规则的子串的字符组合。一段用于匹配的正则表达式通常称为一个模式。JavaScript 中,正则表达式可以通过字面量来定义,使用时会被包装为一个 RegExp 对象。
2. 正则表达式的支持
一种语言对正则表达式的支持分为两个部分:
- 正则表达式本身的语法支持
- 正则表达式的运行与匹配支持,在 JavaScript 中,主要是
RegExp对象的方法和String对象的部分方法。
第一种对语法的支持是绝对的,不以语言不同而转移,第二种运行支持是相对的,要依赖正则语法和程序语言支持。
3. 简单模式
简单模式就是直接由想要匹配的字符串组成。也仅仅能严格匹配到相等的字符串,这里区分大小写和字符顺序。
/Str/.exec('strStrsTR') // Str
JavaScript中,一般情况下,如果一个参数的要求是正则表达式,而实际接收到了一个字符串,那么字符串会被转化成简单模式的正则表达式。
4. 元字符
如果想要匹配模式更加灵活,必须依赖元字符。元字符也是一组字符串,但是不再表达本身字符的意思。元字符分可以有很多种,为方便理解,我把元字符分为下述几种:
- 简单字符
- 字符集
- 量词
- 位置
- 文本简写字符
- 文本控制字符
二 元字符
1. 简单字符
简单匹配字符作用于单个字符上。
| 匹配字符 | 描述 | |
|---|---|---|
. | 匹配除了换行符外的任意单个字符 | |
| ` | ` | 或运算符 |
\ | 转义运算符,用于匹配保留的字符 `[] () . * + ? ^ $ \ | ` |
(1) 点号
/./.exec('jsx') // j
(2) 或运算符
// exec() 方法只执行一次,match 方法可以通过 g 标志,实现全局匹配
'SsDdDk'.match(/s|D/g) // s,D,D,
(3) 转义运算符
/\//.exec('s//d/k') // /,
2. 字符集
字符集用于匹配集合内的所有字符。默认情况下,严格区分大小写。
| 字符集 | 描述 |
|---|---|
[] | 字符集,匹配方括号内的任意字符,如 [xyz] 就是匹配 x,y,z 中任意一个字符 |
[^] | 否定字符集,匹配除了方括号里的任意字符 |
常用字符集包括:
[a-z]: 匹配 a-z 之间任意字符,也就是匹配所有小写字母[A-Z]: 匹配 A-Z 之间任意字符,也就是匹配所有大写字母[0-9]: 匹配 0-9 之间任意字符,也就是匹配所有数字
(1) []字符集
/[uH2]/.exec('UH34') // H
(2) [^]否定字符集
/[^Uh3]/.exec('Uh23') // 2
3. 量词
量词用于规定匹配的次数。默认情况下,每一个规则或者说元字符,仅仅匹配一次,且只作用于单个字符上。可以通过量词来描述期望的匹配次数。
| 量词 | 描述 |
|---|---|
* | 匹配星号前的字符或子模式,大于等于 0 次。隐藏的一个问题,如果一串字符的开头不匹配,那么也不会往下继续匹配,因为规则上允许匹配0次 |
+ | 匹配加号前的字符或子模式,大于等于 1 次。隐藏的一个含义,如果当前没匹配到子串,就回继续查找,如果匹配到一段符合规则的子串,会继续往后,直到查到第一个不符合规则的子串的时候结束 |
{n} | 匹配 n 次 花括号前的字符或子模式 |
{n,} | 匹配 大于等于 n 次 花括号前的字符或子模式 |
{n,m} | 匹配 i 个花括号前的字符,i 大于等于 n 且小于等于 m |
(1) 星号
// 匹配大于等于 0 个星号之前的字符或子模式。关联字符时,只关联模式串中的星号前一个字符,之前的字符会按照相应的匹配规则
// 第一个规则,模式串前半部分的字符是简单模式,也就是要求严格匹配
// 第二个规则,匹配方括号字符集里面的任意一个,
// 由于默认的匹配类型是贪婪匹配,因此会尽可能多的向后匹配,直到遇到不匹配的字符
// 第三个规则,意思为匹配 a-z 之间的小写字母,也就是所有的小写字母,但第二个字符是大写 S,因此只匹配了第一个字符
/Sd*/.exec('sdSdddK') // Sddd
/[sSD]*/.exec('sSDSkDdk') // sSDS
/[a-z]*/.exec('sSDSDdk') // s
(2) 加号
// 下面的正则要求匹配一次及以上的字符 d
// 由于加号至少要求匹配一次,因此,第一个字母不是 d,不满足条件,会继续向后找,匹配到 ddd
// ddd 匹配完后,已经具备了匹配到一个子串的要求,因此,发现 k 不符合规则后,直接返回
/d+/.exec('sdddk') // ddd
(3) 花括号
// 匹配一次
/D{1}/.exec('sDDDDk') // D
// 匹配大于等于一次
/D{1,}/.exec('sDDDDk') // DDDD
// 大于等于1次,小于等于3次
/D{1,3}/.exec('sDDDDk') // DDD
4. 位置
| 位置 | 描述 |
|---|---|
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
\b | 匹配一个词的边界 |
\B | 匹配一个非单词边界 |
如何理解边界? 一个词的边界就是不跟随其他词的字符的位置。如单词之间的空格,一行文字的开头和结尾,以及不算“字”的字符等等。 JavaScript 中的 “字”,或 “词”,与我们日常理解的有些不同。 JavaScript 中的字只包含 大写字母,小写字母,十进制数字,下划线。 除此之外的字符,都可以算作打断一个单词。
位置元字符,只提供位置的约束和规则,不匹配字符。
(1) 开头
// 匹配字符串开头的一个斜杠
// 斜杠是 \/ 匹配到的,^ 匹配的是位置规则。
/^\//.exec('//s/d/k') // /
// 由于开头的字符不是 /,而是 s,因此匹配不到
/^\//.exec('s/d/k') // null
(2) 结尾
// 匹配字符串结尾的一个斜杠
/\/$/.exec('sd/k//') // /
// 由于结尾的字符是 k,不是斜杠,因此匹配不到
/\/$/.exec('sd/k') // null
(3) 边界
// 第一个规则匹配一个在结尾边界前的字符,在这里就是 b
/\w\b/.exec('ab') // b
// 这个规则匹配一个开始边界后的字符,在这里就是 a
/\b\w/.exec('a b') // a
// 这个规则匹配全局匹配一个开始边界后的字符,d 前有 c,不是开始边界后的字符,因此没有匹配到
'a b-cd'.match(/\b\w/g) // a,b,c,
// 这段规则什么也匹配不了,因为位置元字符不匹配字符,因此,这段规则可以简化成下面的规则:
// \w\w,匹配两个连起来的字母,数字,下划线,但是又要求这两个字符之间存在边界,这显然是不存在的
/\w\b\w/.exec('a b') // null
(4) 非边界
// 匹配一个不是开头边界的字符,这里 a 是,c 是,只有 b 不是
'ab-c'.match(/\B\w/g) // b
// 这条规则什么也匹配不到,因为之前说过,位置元字符只匹配位置,不匹配字符
// 不存在一个字符,既是边界,又不是边界
'ab-c'.match(/\b\B\w/g) // null
5. 文本匹配字符
| 文本匹配字符 | 描述 |
|---|---|
\w | 匹配字母,数字和下划线 |
\W | 匹配非字母,数字和下划线 |
\d | 匹配数字 |
\D | 匹配非数字 |
\s | 匹配所有空白符 相当于 [\t\n\f\r\p{Z}] |
\S | 匹配所有非空白符 |
(1) \w 和 \W
/\w+/.exec('J2s_皆X') // J2s_
/\W+/.exec('df好卡啊z卡') // 好卡啊
(2) \d 和 \D
/\d/.exec('g2') // 2
/\D+/.exec('12san4') // san
(3) \s 和 \S
/\s/.exec('1 7 ') // [空格] 由于只有一个 \s,因此只匹配一个空格
/\S+/.exec(' Jk') // Jk
6. 文本控制字符
| 文本控制字符 | 描述 |
|---|---|
\f | 匹配一个换页符 |
\n | 匹配一个换行符 |
\r | 匹配一个回车符 |
\t | 匹配一个制表符 |
\v | 匹配一个垂直制表符 |
\p | 匹配 DOS 命令行终止符,相当于 \r\n |
三 零宽断言
普通元字符用来匹配某个特定的字符,而零宽断言用来匹配特定的 位置,指明匹配的字符前后必定有满足断言规则的内容。它包含两个概念:
- 断言: 即断定某件事,某种情况,必定发生。这里的断言,用来判断某个字符的前后,是否符合某种特定格式。
- 零宽: 即不占用宽度。也就是说,断言匹配的是字符前后的位置,不占据字符。如果断言前后还有其他规则,不会跳过断言匹配的字符。断言规则在匹配结果中也不返回
注: 在断言规则的写法上,先行断言是与断言前面的模式组合,后行断言与断言后面的模式组合
零宽断言种类
| 零宽断言 | 描述 |
|---|---|
?= | 正先行断言。正,正向,理解为真,即匹配符合规则的位置,先行,即匹配断言规则前面的字符 |
?! | 负先行断言。负,反向,理解为假,即匹配不符合规则的位置,先行,即匹配断言规则前面的字符 |
?<= | 正,正向,理解为真,即匹配符合规则的位置,后行,即匹配断言规则后面的字符 |
?<! | 负,反向,理解为假,即匹配不符合规则的位置,后行,即匹配断言规则后面的字符 |
1. 正先行断言
// 正先行断言
// 正,正向,理解为真,即匹配符合规则的位置,先行,即匹配断言规则前面的字符
// 下面的匹配由若干数字开头,结尾是dk的字符串,再去掉断言规则本身,返回的是 233
/\d+(?=dk)/.exec('sdkS233dk') // 233
// 下面两个例子为了验证零宽
// 零宽的意思是,断言不占据位置,断言规则不会过滤掉字符,
// 也就是说,如果匹配到了一段符合规则的子串,断言返回的子串不包含断言规则匹配的部分
// 但如果后续还有正则规则,会从断言规则前的位置开始
// 第二个返回1d,第三个返回1,可以看出,d是由\w匹配到的,因为断言本身不占据位置,所以\w从1后面的d开始匹配
/1(?=d)\w/.exec('1d_') // 1d
/1(?=d)/.exec('1d_') // 1
2. 负先行断言
// 负先行断言
// 负,反向,理解为假,即匹配不符合规则的位置,先行,即匹配断言规则前面的字符
// 这里的匹配规则为:前方有若干字符(字母,数字,下划线),后面跟着两个井号。
// 从左向右匹配,第一个不符合规则的位置为d_k2D,因为这串字符后面跟着的是K而不是##
// 最后,断言不匹配规则本身,因此返回的值为d_k2D
/\w+(?!##)K/.exec('##d_k2DK##$DK') // d_k2DK
3. 正后行断言
// 正后行断言
// 正,正向,理解为真,即匹配符合规则的位置,后行,即匹配断言规则后面的字符
// 第一个规则,匹配两个井号(##)开头,后面跟若干数字的字符串,匹配结果不含井号
/(?<=##)\d+/.exec('1##233###4#') // 233
// 第二个规则,匹配两个$#开头,且后续跟若干数字的字符串,
// 由于后行断言规则前仍有正则规则,因此前面的字符被其他的规则匹配到,返回$#233
/\$#(?<=\$#)\d+/.exec('1$#233###4#') // $#233
4. 负后行断言
// 负后行断言
// 负,反向,理解为假,即匹配不符合规则的位置,后行,即匹配断言规则后面的字符
// 匹配一个不以两个井号开头的若干数字组成的字符串,返回 45
/(?<!##)\d+/.exec('##2#45#') // 45
四 标记
标记用来修改正则表达式的一些默认行为,包括是否忽略大小写(i),是否全局搜索(g),是否多行搜索(m)。
标记不属于元字符,因此不写在正则规则里面,而是写在规则后面。
// 全局匹配所有的字母,数字,或下划线
/\w/g.exec('sdk') // sdk
| 标记 | 描述 |
|---|---|
i | 忽略大小写 |
g | 全局查询搜索匹配 |
m | 多行标记m可以让开头和结尾的标记符 ^, $ 在多行使用,同样的,全局匹配多次的话,需要加上全局标记 g |
1. 忽略大小写
// 匹配至少两个或以上的小写字母,忽略了大小写,因此大写字母也能匹配的到
/[a-z]{2,}/i.exec('sDK') // sDK
2. 全局搜索
// 这条规则匹配若干数字,字母,或下划线,后面紧跟一个空白符,而 sdk 后面没有空白符,因此匹配不到
"s sd sdk".match(/\w+\s/g) // s ,sd ,
3. 多行匹配
// 全局查询,且多行查询,规则要求前面若干字符,后续跟一个井号,且井号不返回
`
sdk#
sDK#
SDK!
`.match(/\w+(?=#)/gm) // sdk,sDK
// 匹配到最后一个字符,并且这个字符要是井号,且多行匹配,但只匹配一次
`
sdk!
sDK#
sDK#
SDK@
`.match(/#$/m) // #
五 贪婪匹配和懒惰匹配
正则表达式默认情况下是贪婪匹配。意味着正则会尽可能地匹配多的符合规则地字符。这包含这以下几个意思:
- 对于单个匹配字符,字符集等,不加量词的情况下,如
\w,它的作用是匹配一个数字,字母,或下划线。因此,即便存在再多地字母,也只会匹配一个字母。 - 贪婪的地方在于,会尽可能的在某一个已经匹配规则的子串后面,如果下一个字符符合规则,会继续匹配相邻的下一个字符,否则就返回现有字符。而不是会匹配到整个字符串所有符合规则的字符串(此时需要用全局标记
g)。 - 懒惰的地方在于,只匹配刚好满足规则的子串,不进行过多匹配。
1. 惰性匹配
在元字符,或者子模式后使用?将紧邻的前面的规则修饰为惰性匹配
// 贪婪匹配
/\w+/.exec('sdk_ s') // sdk_
// 惰性匹配
/\w+?/.exec('sdk_ s') // s
六 子模式
子模式是用括号()括起来的一组规则。子模式会被记忆下来,使用 带全局标记的 exec() 和 不带全局标记的 match() 匹配时会单独匹配一份。
// 这个规则匹配一个数字后跟随多个字符,并且最后要有至少一个井号 #
const reg = /(\d\w+)#+/g;
const str = '2d3c-#5g##8ff';
// exec() 方法会将正则执行一次,如果正则给出了全局标志位 g,那么返回的数据是可迭代的
// 可以多次运行获取后面的所有匹配结果
reg.exec(str);
// ["5g##", "5g", index: 6, input: "2d3c-#5g##8ff", groups: undefined]
// 可以看出,exec(),执行一次的返回结果的前两个数组索引处,都是匹配的结果
// 第一个是整个模式串匹配的结果,第二个位置是子模式单独匹配的结果
除了 exec() 方法以外,子模式主要起到的就是一个分组的作用。也就是说,默认的情况下,一个修饰符或者一个量词,只修饰前面的一个字符股。比如说,\w\d+ 这个规则是只匹配一个字符,这个字符后面跟着至少一个数字。如果想要改成匹配字符和数字的多个组合呢?就可以使用子模式:
'2d3f4--f6s'.match(/(\w\d)+/g) // ["d3f4", "f6"]
七 JavaScript 中如何运行正则
JavaScript 中主要使用 String 对象和 RegExp 对象提供的方法来运行正则表达式。
1. RegExp 对象的方法
| 方法名(RegExp.prototype.) | 描述 |
|---|---|
| exec(string) => array | exec() 方法返回正则执行一次的结果。如果设置了全局标志g,会返回一个捕获组,用于迭代执行。且这个捕获组的第一项是当前完整正则的一次执行结果,第二项开始是每一个子模式的执行结果。 |
test(string) => boolean | test() 方法用于检测传入的字符串是否包含符合正则规则的子串,如果包含返回 true,否则返回 false。 |
2. String 对象的方法
| 方法名(String.prototype.) | 描述 |
|---|---|
match(regex) | match() 方法接收一个正则对象,用来匹配调用字符串中符合规则的子串。如果正则对象添加了全局标志,那么直接返回所有的匹配结果,否则,返回一个可以被迭代执行的捕获组。(同 exec()) 。 |
search(reg | str) => index | -1 | search() 方法接收一个正则对象或一个字符串(字符串会转换成简单模式的正则表达式),用来查找调用字符串中符合规则的子串的位置,匹配到了就返回第一次出现的索引,否则返回 -1 |
replace(reg | str, newStr | fn) => replacedStr | replace() 方法接收两个参数,第一个参数是用来匹配调用字符串的正则表达式(字符串会被转化成简单模式的正则规则),第二个参数表示要替换的字符串或者返回字符串的函数。replace() 方法不改变源字符串。 |
split(reg | str, limit) => array | split() 方法接收两个参数,第一个是一个正则或字符串,表示将要匹配并用于切分的字符,使用第一个参数匹配调用字符串,在符合规则的位置将字符串切分未为数组,并返回。第二个参数限制分割的片段数量。 |