参考资料:
正则表达式(regular expression)是用来表达字符串结构的一种模式,常用来匹配字符串(即文本),判断字符串中是否有给定结构的部分。
JavaScript 中的正则表达式借鉴了 Perl 语言,内置了 RegExp 对象提供正则表达式的功能,这里主要记录下正则表达式的匹配规则。
1. 字符分类
正则表达式字面上是斜杆 / 之间的部分,由字符组成,不过里面的字符有的是字面上的意思,有的却有特殊作用。
/regular expression/igm
注:i g m 为修饰符,用于附加一些规则。ES6 中还增加了 u y s 修饰符。
我们把表示字面意思的字符称为 字面量字符,有特殊作用的字符称为 元字符。
下面主要对 元字符 进行记录:
(1) 点字符(.)
点字符(.)表示任意字符,除了回车(\r)、换行(\n)、行分隔符(\u2028)、段分隔符(\u2029),以及码点大于 0xFFFF 的字符。
注:
ES6中推出的u修饰符,能匹配上码点大于0xFFFF的字符。
/a.c/.test('abc') // true
/a.c/.test('abbc') // false
注意,字符指的是单个字符,而不是多个,除了转义的字符,字面上看着是多个,实际上也还是表示一个。
(2) 位置字符(^ $)
位置字符 ^ 表示字符串的开始位置,而 $ 表示字符串的结束位置。
// 字符串中有以 abc 开头的部分
/^abc/.test('abcdef') // true
// 字符串中有以 def 结尾的部分
/def$/.test('abcdef') // true
// 字符串从开头到结尾只有 abc
/^abc$/.test('abc'); // true
(3) 选择符(|)
选择符(|)在正则表达式中表示 “或”,比如 /cat|dog/ 表示匹配 cat 或 dog。
// 字符串中有 cat 或 dog
/cat|dog/.test('cat') // true
/cat|dog/.test('dog') // true
注意,选择符
|会包含前后多个字符,而不是单个字符。
(4) 转义字符(\)
如果要在正则表达式中表示 元字符,就得进行转义,即跟在 \ 后面,比如想表示点符号(.):
// 字符串中有 a.c
/a\.c/.test('a.c') // true
/a\.c/.test('abc') // false
注意,如果正则表达式是以构造函数生成的,由于第一个参数是字符串,所以本身会转义,得用两个转义符
\\才行。
var r = new RegExp('a\\.c'); // /a\.c/
(5) 集合类([])、脱字符(^)、连字符(-)
集合类([])用于表示有一系列字符可用来匹配,相当于一个字符集合,将字符都放到 [ 和 ] 之间。
// 字符串中有 a、b、c 其中一个字符
/[abc]/.test('a'); // true
/[abc]/.test('b'); // true
/[abc]/.test('c'); // true
集合类中,^ 不是位置字符,而是脱字符,表示集合类之中的字符都不用来匹配。
// 字符串中没有 a、b、c 任意一个字符
/[^abc]/.test('a'); // false
/[^abc]/.test('b'); // false
/[^abc]/.test('c'); // false
注意,脱字符
^必须在集合类的第一个位置才算。[^]表示匹配任意字符。
集合类中,- 也算特殊字符了,称为连字符,表示连续的字符集合。
// 字符串中有 a 到 z 小写字母其中一个字符
/[a-z]/.test('b') // true
// 字符串中有数字、小写字母、大写字母其中一个字符
/[0-9a-fA-F]/.test(1) // true
注意,连字符
-只对单个字符生效,所以[1-31]代表从1到3的字符,而不是从1到31。另外也可以对Unicode字符生效。
// 字符串里有 1、2、3 其中之一
/[1-31]/.test(4) // false
// 字符串里有从 \u0128 到 \uFFFF 其中之一
/[\u0128-\uFFFF]/.test('\u0130\u0131\u0132') // true
(6) 重复类({})、量词符(? * +)、贪婪模式
重复类({})用于表示一个字符的重复次数(即出现次数)。
-
{n}:字符重复n次。 -
{n,}:字符重复至少n次。 -
{n,m}:字符重复n到m次。
// 字符 o 重复两次
/lo{2}/.test('look') // true
/lo{2}/.test('lok') // false
正则表达式中还可以用量词符来表示字符的出现的次数。
-
?:字符出现0次或1次,相当于{0,1}。 -
*:字符出现至少0次,相当于{0,}。 -
+:字符出现至少1次,相当于{1,}。
// 字符 t 出现 0 次或 1 次
/t?est/.test('test') // true
/t?est/.test('est') // true
通常情况下,指定了字符的出现次数后,都是按最多的情况去匹配,这一规则称为 贪婪模式。
// 贪婪模式:字符 a 出现 1 到 4 次,则按最多的情况返回
/a{1,4}/.exec('aaaabc')[0] // "aaaa"
为了匹配更少的,可以采用 非贪婪模式,通过在后面跟上 ? 就可以。
// 非贪婪模式:字符 a 出现 1 到 4 次,则按最少的情况返回
/a{1,4}?/.exec('aaaabc')[0] // "a"
(7) 不可打印字符
正则表达式对一些不可打印的特殊字符,提供了表达方式:
-
\cX:表示Ctrl-[X],其中的X是A-Z之中任一个英文字母,用来匹配控制字符。 -
[\b]:匹配退格键(U+0008),不要与\b混淆。 -
\r:匹配回车键。 -
\n:匹配换行键。 -
\t:匹配制表符 tab(U+0009)。 -
\v:匹配垂直制表符(U+000B)。 -
\f:匹配换页符(U+000C)。 -
\0:匹配null字符(`U+0000``)。 -
\xhh:匹配一个以两位十六进制数(\x00-\xFF)表示的字符。 -
\uhhhh:匹配一个以四位十六进制数(\u0000-\uFFFF)表示的Unicode字符。
(8) 预定义字符
预定义字符是对正则表达式中一些常用字符匹配模式的简写。
-
\d:匹配0到9中的任一数字,相当于[0-9]。 -
\D:匹配0到9以外的字符,相当于[^0-9]。 -
\w:匹配任意的字母、数字和下划线,相当于[a-zA-Z0-9_] -
\W:匹配字母、数字和下划线以外的字符,相当于[^a-zA-Z0-9_] -
\s:匹配空格(包括换行符、制表符、空格符等),相当于[\r\n\t\v\f]。 -
\S:匹配非空格的字符,相当于[^\r\n\t\v\f]。 -
\b:匹配词边界,表示词独立。 -
\B:匹配非词边界,表示词不独立。
// 单词 world 独立
/\bworld/.test('hello world') // true
/\bworld/.test('world hello') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('world-hello') // true
/world\b/.test('world hello') // true
/world\b/.test('hello world') // true
/world\b/.test('hello-world') // true
/world\b/.test('world-hello') // true
// 单词 world 不独立
/\Bworld/.test('hello world') // false
/\Bworld/.test('helloworld') // true
/world\B/.test('hello world') // false
/world\B/.test('helloworld') // true
(9) 修饰符(i g m u y s)
修饰符放在正则表达式斜杠 / 后面,用于附加一些规则:
-
i:忽略大小写(ignore) -
g:全局匹配(global),只要剩余位置上有匹配就行,使用字符串的match方法时返回每次匹配的内容形成的数组,而不会返回分组的内容。 -
m:允许多行(multiline),只影响位置字符^和$。 -
u:Unicode模式,用来正确匹配码点大于\uFFFF的字符 -
y:粘连(sticky),确保多值匹配时,从剩余的第一个位置能匹配上 -
s:单行(singleline),又称dotAll模式,点字符(.)代表了所有的字符,包括换行符\n等。
// i 忽略大小写
/abc/.test('ABC') // false
/abc/i.test('ABC') // true
// g 全局匹配
'abbcbb'.match(/bb/) // ["bb", index: 1, input: "abbcbb", groups: undefined]
'abbcbb'.match(/bb/g) // ["bb","bb"]
// m 允许多行
/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true
// u Unicode 模式
'🐪'==='\uD83D\uDC2A' // true
/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true
// y 粘连(sticky),确保多值匹配时,从剩余的第一个位置能匹配上
var str = 'aaa_aa_a';
var reg1 = /a+/g;
reg1.exec(str) // ["aaa"]
reg1.exec(str) // ["aa"]
var reg2 = /a+/y;
reg2.exec(str) // ["aaa"]
reg2.exec(str) // null
// s 单行(singleline),又称 dotAll 模式,点字符(.)代表了所有的字符
/a.c/.test('a\nc') // false
/a.c/s.test('a\nc') // true
2. 分组匹配
正则表达式里可以用小括号 () 将多个字符分为一组,以组为单位进行匹配,而不是单个字符进行匹配。
// 字符 p 重复至少 1 次
/group+/.test('groupp') // true
// 字符组 group 重复至少 1 次
/(group)+/.test('groupgroup') // true
(1) 捕获
正则表达式里使用分组后,通过字符串的 match 方法在非全局匹配的情况下,会捕获每个分组所匹配的内容:
// 非全局匹配时,match 返回数组,第一个元素是整体匹配内容,后续的元素是分组匹配的内容
'abc'.match(/(.)b(.)/) // ["abc","a","c"]
// 全局匹配时,会把匹配到的字符串返回,而不会返回分组匹配的内容
'abc'.match(/(.)b(.)/g) // ["abc"]
如果想要一个分组不被 match 方法捕获,则可以使用 (?:) 的分组形式,称为 非捕获组。如下:
// 采用非捕获组,则不会捕获相应分组匹配的内容
'abc'.match(/(?:.)b(.)/) // ["abc","c"]
正则表达式里还可以用 \n (n >= 1 且为整数)的形式来引用分组,表示和分组匹配的内容一致:
// 字符 b 前面的字符一样,后面的字符也一样
/(.)b(.)\1b\2/.test("abcabc") // true
// 小括号还可以嵌套,\1 和外层括号匹配内容一致,\2 和内层括号匹配内容一致
/y((..)\2)\1/.test('yabababab') // true
(2) 断言
断言指的是直接指定一个字符前面或后面会是什么,或者不是什么,主要分为 4 种:
x(?=y):先行断言(x后面是y才匹配)
// b 后面是 c 才匹配
/ab(?=c)/.test('abc') // true
/ab(?=c)/.test('ab') // false
/ab(?=c)/.test('abd') // false
x(?!y):先行否定断言(x后面不是y才匹配)
// b 后面不是 c 才匹配
/ab(?!c)/.test('abc') // false
/ab(?!c)/.test('ab') // true
/ab(?!c)/.test('abd') // true
(?<=y)x:后行断言(x前面是y才匹配)
// b 前面是 a 才匹配
/(?<=a)bc/.test('abc') // true
/(?<=a)bc/.test('bc') // false
/(?<=a)bc/.test('dbc') // false
(?<!y)x:后行否定断言(x前面不是y才匹配)
// b 前面不是 a 才匹配
/(?<!a)bc/.test('abc') // false
/(?<!a)bc/.test('bc') // true
/(?<!a)bc/.test('dbc') // true
注意,先行(
lookahead)需要判断后面是不是,后行(lookbehind)需要判断前面是不是。而为什么叫法感觉不对,因为参照物是分组,我要匹配的内容在分组前面,那么就是先行,反之是后行。
(3) 具名
ES6 中提出了将分组进行命名,方便读取匹配结果,即 具名分组匹配。
形式为在分组内最前面加上 ?<组名>,如:
/(?<数字>\d+)/.exec('ab12c3');
// ["12", "12", index:2, input: "ab12c3", groups:{"数字":"12"}, length:2]
可以看到匹配结果中,groups 对象会将具名分组的名称作为键,匹配结果作为值,如果没有具名分组,则 groups 为 undefined。
有了具名分组后,使用字符串的 replace 方法进行替换时,可以用 $<组名> 来表示分组所匹配的内容:
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>') // "02/01/2015"
而当我们想在正则表达式中,引用下具名分组,则可以用 \k<组名> 来引用分组,当然 \n (n >= 1 且为整数)的形式也支持:
/^(?<word>[a-z]+)!\k<word>$/.test('abc!abc') // true
/^(?<word>[a-z]+)!\1$/.test('abc!abc') // true
(完)