正则引擎
正则的引擎大致可分为两类:DFA和NFA
- DFA (Deterministic finite automaton) 确定型有穷自动机
- NFA (Non-deterministic finite automaton) 非确定型有穷自动机,大部分都是NFA
这里的“确定型”指,对于某个确定字符的输入,这台机器的状态会确定地从a跳到b,“非确定型”指,对于某个确定字符的输入,这台机器可能有好几种状态的跳法;这里的“有穷”指,状态是有限的,可以在有限的步数内确定某个字符串是否满足条件;这里的“自动机”指,一旦这台机器的规则设定完成,就可以自行判断了,不要人看。
DFA引擎不需要进行回溯,所以匹配效率一般情况下要高,但是它并不支持捕获组,于是也就不支持反向引用和$这种形式的引用,也不支持环视(Lookaround)、非贪婪模式等一些NFA引擎特有的特性。
声明方式
- 使用构造函数
var reg = new RegExp("<style[^>]*>([\\s\\S]*)</style>","g")
此时其中\
视为字符串\
,如需使用其他功能需要转义\\
- 直接声明
var reg = /<style[^>]*>([\s\S]*)<\/style>/g
此时其中字符串/
需要\/
进行转义
字符和位置
- 如果一个子正则表达式匹配到的是字符,而不是位置,而且会被保存到最终的结果中,那个这个子表达式就是占有字符的,比如/ha/(匹配ha)就是占有字符的;
- 如果一个子正则匹配的是位置,而不是字符,或者匹配到的内容不保存在结果中(其实也可以看做一个位置),那么这个子表达式是零宽度的,比如/read(?=ing)/(匹配reading,但是只将read放入结果中),其中的(?=ing)就是零宽度的,它本质代表一个位置。
占有字符是互斥的,零宽度是非互斥的。一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。举个栗子,/aa/是匹配不了a的,这个字符串中的a只能由正则的第一个a字符匹配,而不能同时由第二个a匹配;但是位置是可以多个匹配的,比如/\b\b?=a/是可以匹配a的,虽然正则表达式里有3个零宽度的子表达式,是可以同时匹配位置0的。
修饰符
- i 忽视大小写。
- g 全局匹配。
不启用全局匹配时,match方法返回的数组第一项为第一个匹配内容,后续为各个捕获组
启用全局匹配时会返回所有匹配内容,但不包括捕获组 - m 将
^
和$
变为匹配单行的开始和结尾。(并非有m时才能多行匹配) - s (ES2018新增修饰符)启用dotALl模式,允许
.
匹配一切字符
简单元字符
- ^ 脱字符,表示文本的开始(有m时则为行初)
- $ 表示文本的结束(有m时则为行末)
一般来说,反斜杠把元字符还原为普通字符,但是有一些普通字符,带反斜杠后反而变成了元字符。
- \b 匹配一个单词边界(boundary,匹配一个位置,该位置前后不全是\w能描述的字符且不是中文。通常匹配单词和空格之间的位置,包括开头结尾)
- \B 匹配一个非单词边界
- \d 匹配一个数字字符(digit)
- \D 匹配一个非数字字符
- \s 匹配一个空白字符(space,
空格
和\f\n\r\t\v的超集)- 其中\f是换页符,\n是换行符,\r是回车符,\t是水平制表符,\v是垂直制表符,除空格和\n外均为不可打印字符,因此可以认为匹配[\n| ]
- \S 匹配一个非空白字符
- \w 匹配一个字母或者一个数字或者一个下划线(word)
- \W 匹配一个字母、数字和下划线之外的字符
-
.
可以匹配换行符之外的任意单个字符。
(.|\n)
和[\b\B]
和[\d\D]
和[\s\S]
和[\w\W]
是等价的。
量词 (它重复紧贴在它前面的某个集合。)
- ? 重复零次或者一次
- + 重复一次或者多次,也就是至少一次
- * 重复零次或者多次,也就是任意次数
- {n} 重复n次
- {n,} 重复n次或者更多次
- {n,m} 重复n次到m次
转义
任何在正则表达式中有作用的字符都建议转义,哪怕有些情况下不转义也能正确,比如[]中的圆括号、^符号等。
优先级问题
优先级从高到低是:
- 转义 \
- 括号(圆括号和方括号)(), (?:), (?=), []
- 字符和位置
- 竖线 |
贪婪模式与非贪婪模式
- 紧贴在量词后的
?
可以开启非贪婪模式
不带问号的限定符也称匹配优先量词,带问号的限定符也称忽略匹配优先量词。
字符组与分歧
- []中的字符集合只是所有的可选项,最终它只能匹配一个字符。
此时$
&
@
()
等元字符只视为普通字符,不需要转义。 -
^
在字符组中表示取反,不再是文本开始的位置了。 -
-
连字符,匹配范围在它的左边字符和右边字符之间(如[0-9]
)。只有字母和数字可以用连字符。
分歧 :
[(ab)(cd)]并不会用来匹配字符串“ab”或“cd”,而是匹配a、b、c、d、(、)这6个字符中的任一个,也就是想表达“匹配字符串ab或者cd”这样的需求不能这么做,要这么写ab|cd
捕获组与非捕获组
()
将其中的字符集合打包成一个集合,供量词操作。且其内容可以被捕获。
- 正则内捕获
使用\数字
的形式,从\1
开始,分别对应前面的圆括号捕获的内容(从左向右,从外向内)
。这种捕获的引用也叫反向引用
。
'<App>hello regex</App><p>A</p><p>hello regex</p>'
.match(/<((A|a)pp)>(hello regex)+<\/\1><p>\2<\/p><p>\3<\/p>/);
- 正则外捕获
RegExp是构造正则的构造函数。
RegExp会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面,它的实例属性$数字
会显示对应的引用。
const r = /^(\d{4})-(\d{1,2})-(\d{1,2})$/
r.exec('2019-10-08')
console.log(RegExp.$1) // 2019
console.log(RegExp.$2) // 10
console.log(RegExp.$3) // 08
另外在replace
方法中也支持引用
'hello **regex**'.replace(/\*{2}(.*)\*{2}/, '<strong>$1</strong>');
// "hello <strong>regex</strong>"
- 非捕获组
只要在圆括号内最前面加上?:
标识,即不会被捕获,可以节省性能
捕获命名 (ES2018的新特性)
- 在捕获组内部最前面加上
?<key>
,它就被命名了。使用\k<key>
语法就可以引用已经命名的捕获组。
let res = '<App>hello regex</App>'.match(/<(?<tag>[a-zA-Z]+)>.*<\/\k<tag>>/);
res.groups.tag // App
零宽断言
注意,2023年4月为止 safari不支持零宽断言!!!请勿在微信web开发中使用。
零宽断言简称断言,别名环视、巡视,用于匹配一个位置而非字符。断言内容本身需处于括号内,但不会被捕获为捕获组。
- 肯定先行断言 : 为紧贴在它前面的规则服务。如
x(?=y)
匹配后面有y的x
'CoffeeScript JavaScript javascript'.match(/\b\w{4}(?=Script\b)\w/);
// ['JavaS', index: 13, input: 'CoffeeScript JavaScript javascript', groups: undefined]
- 肯定后行断言 : 如
(?<=y)x
匹配前面有y的x - 否定先行断言 : 如
x(?!y)
匹配后面不是y的x - 否定后行断言 : 如
(?<!y)x
匹配前面不是y的x
方法
正则方法
- test
返回true/false - exec
如匹配成功,返回数组第一项为第一个匹配的字串,之后为各捕获组的反向引用
如匹配失败,则返回 null。
字符串方法
- match
如匹配成功且无修饰符g,同exec
如匹配成功且有修饰符g,返回数组各项依次为匹配的各字符串,不返回捕获组
如匹配失败,则返回 null。