正则引擎的匹配规则
NFA
引擎最重要的性质就是支持回溯
,那么哪些情况下发生回溯呢?会发生回溯的情况有两种:
- 匹配优先/忽略优先/占有优先量词;
- 多选分支结构。
回溯机制
回溯机制的基本原理不难理解,记住两个基本规则:
规则一:面对众多分支选择时,哪个分支应当首先选择?
若需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,而对于忽略优先量词,会选择“跳过尝试”。
规则二:回溯时,应选择哪个保存的状态?
距离当前最近储存的选项(即备用状态)就是本地失败强制回溯时返回的。
对于NFA
引擎来说,“备用状态”是引擎进行回溯的基础,“备用状态”会保存两个位置:正则表达式中的位置和未尝试的分支在字符串中的位置。在需要回溯时,“备用状态”告诉NFA
引擎,匹配可以从此处开始。
来看一些简单的例子:
匹配未进行回溯
使用正则表达式/Lu?v/
匹配字符串Luv
:当字符L
匹配成功后,匹配的当前状态如下:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Lu?v |
现在该u?
匹配了,此时正则引擎需要决定:是尝试匹配呢?还是跳过匹配?因为?
是匹配优先量词
,所以引擎会尝试匹配。但为了确保这个尝试最最终失败后能够恢复,NFA
引擎会把以下状态添加到备用状态序列
中:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Lu?v |
该备用状态
告诉引擎:下次匹配从正则表达式的u?
之后,字符串的字符v
之前开始匹配(即跳过u
匹配)。之后继续检查字符 u
,能够匹配,状态更新为:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Lu?v |
继续检查字符v
,也能满足匹配,此时整个匹配完成,备用状态
不在被需要,弃用备用状态
,整个匹配结束。
匹配进行了回溯
用上例相同的正则表达式匹配字符串Lv
,在尝试u
之前,一切与上例过程相同,但此时的u
无法匹配,引擎开始回溯至最近保存的状态(即该例中唯一的备用状态
),即:
字符串状态 | 表达式状态 |
---|---|
'Lv' | Lu?v |
此时v
满足匹配,整个匹配结束。
进行了回溯但匹配失败
整个匹配宣告失败是发生在字符串和表达式均完成了所有回溯测试(备用状态
)之后,而不是其中某一个完成其所有回溯测试之后。
举个简单的例子,我们用同样的正则表达式匹配字符串Lue
,在尝试匹配字符u
之前有备用状态
:
字符串状态 | 表达式状态 |
---|---|
'Lue' | Lu?v |
在u
匹配成功后,由于v
无法匹配e
,此时引擎回溯到最近保存状态,交换
字符u
给v
匹配,依然不能满足匹配,该次回溯测试失败,但是此时不存在其它备用状态
,所以字符串中当前位置的整个匹配宣告失败。
那么整个匹配就要宣告失败了吗?并不会!
,匹配将重新开始于状态:
字符串状态 | 表达式状态 |
---|---|
'Lue' | Lu?v |
但这些回溯测试均以失败告终,最终整个表达式宣告匹配失败,结束匹配
忽略优先的回溯匹配
使用正则表达式/Lu??v/
匹配字符串Luv
。字符L
匹配之后的状态:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Lu??v |
现在该u?
匹配了,此时正则引擎需要决定:是尝试匹配呢?还是跳过匹配?因为??
是忽略优先量词
,所以引擎会跳过匹配。但为了确保这个尝试最最终失败后能够恢复,NFA
引擎会把以下状态添加到备用状态序列
中:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Luv |
保存备用状态
后,沿着忽略匹配优先
的路继续匹配:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Lu??v |
但是此时v
无法匹配字符u
,引擎回溯至最近保存状态:
字符串状态 | 表达式状态 |
---|---|
'Luv' | Luv |
此时u
能匹配字符u
,接下来的v
也能匹配字符v
,到此,整个匹配宣告成功,匹配结束。
注意
和第一个例子对比:
const regexp = /Lu?v/; // 匹配优先
// const regexp = /Lu??v/; // 忽略优先
const result = regexp.exec("Luv");
console.log(result[0]); // 结果均为:Luv
发现虽然匹配优先Lu?v
和忽略优先Lu??v
得到的结果可能是(在本例中相同)相同的,但是回溯的过程是完全不同的!