深入学习正则表达式

533 阅读7分钟

正则里括号的用法

1. 分组

分组:正则表达式里括号的表达式为另外一组匹配规则

捕获括号:被匹配的子字符串可以在结果数组的元素 [1]-[n] 中找到,或在被定义的 RegExp 对象的属性 $1-$9 中找到。

代码举例:

let reg = /\d+(\D+)/
reg.exec('123456abcd')
// ["123456abcd", "abcd", index: 0, input: "123456abcd", groups: undefined]
console.log(RegExp.$1)
// "abcd"

在这个正则表达式里我们括号期望的是一组非数字的匹配项,并且执行匹配后可在执行结果的[1]或者RegExp.$1得到匹配值。

正则表达式括号的分组在实际开发中对于我们解决问题有非常大的用处,例如String.replace()这个方法

代码举例:

let str = '123abc'
let reg = /(\d+)(\D+)/
let newStr = str.replace(reg, '$2$1')
console.log(newStr) // abc123
str.replace(reg, function(word, $1, $2){
	console.log(word,$1,$2)
	// word代表字符串在正则匹配到的值, $1代表第一个括号的匹配项, $2代表第二个括号的匹配项
	// 123abc, 123, abc
})

使用正则表达式应用于字符串处理,在上面的例子里我们很容易得就把数字和字母的匹配项互换位置。

在另外一种情况下如果不想要捕获这个匹配项,但是又需要加括号匹配条件,我们可以使用非捕获括号

非捕获括号:匹配项不能够从结果数组的元素 [1]-[n] 或已被定义的 RegExp 对象的属性 $1-$9 再次访问到。

代码举例:

let reg = /\d+(?:\D+)/
reg.exec('123456abcd')
// ["123456abcd", index: 0, input: "123456abcd", groups: undefined]

在例子里执行匹配后括号里的匹配项不会再出现结果里。

2.反向引用

反向引用:一个反向引用(back reference),指向正则表达式中第 n 个括号(从左开始数)中匹配的子字符串。

代码举例:

reg = /(\d+)\D+\1/
reg.exec('123abc123')
// ["123abc123", "123", index: 0, input: "123abc123", groups: undefined]

在正则表达式里\1代表的是\d+,当我们在表达式里有需要重复的时候可以用这种写法。

3.零宽断言

零宽断言:指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。

  1. (?=pattern) 正向先行断言:代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配pattern。
  2. (?!pattern) 负向先行断言:代表字符串中的一个位置,紧接该位置之后的字符序列不能匹配pattern。
  3. (?<=pattern) 正向后行断言:代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配pattern。
  4. (?<!pattern) 负向后行断言:代表字符串中的一个位置,紧接该位置之前的字符序列不能匹配pattern。

正则表达式的括号有时候用来表达断言,具体的细节我们在下面问内容详细说。

贪婪模式与非贪婪模式

贪婪模式与非贪婪模式也是正则里面比较常见的问题了,平时也会经常应用于开发中解决问题。理解贪婪模式和非贪婪模式对我们理解正则引擎执行匹配非常有帮助。

贪婪模式

贪婪模式会匹配尽可能多的字符,贪婪模式用于匹配优先量词修饰的子表达式,匹配优先量词包括:“{m,n}”、“{m,}”、“?”、“*”和“+”

代码举例:

let reg = /\d*/
reg.exec('1234567890')
["1234567890", index: 0, input: "1234567890", groups: undefined]

*号代表匹配任意次数,用大括号代表即{0,},在贪婪模式下尽可能多的匹配,在例子中因为整个字符串完全匹配,所以匹配值为 1234567890。

非贪婪模式

非贪婪模式会匹配尽可能少的字符,在匹配量词后面加上问号就可触发非贪婪模式:“{m,n}?”、“{m,}?”、“??”、“*?”和“+?”

代码举例:

let reg = /\d*?/
reg.exec('1234567890')
// ["", index: 0, input: "1234567890", groups: undefined]

*号代表匹配任意次数,用大括号代表即{0,},因为*号可代表匹配0次,在非贪婪模式下尽可能少的匹配,所以在这个例子里匹配项为空,即不匹配任何字符串。

零宽断言

正则表达式的断言功能非常强大,学习正则的断言应用,对于解决我们开发中的问题提供了新的思路。

在理解断言的执行过程可能会稍微有点绕,但是作为一个开发肯定要有一颗爱折腾的心,哈哈。

下面将只使用正向先行断言来说明断言的执行,其他的三个模式也是大同小异。

先看一个简单例子:

let reg = /abc(?=123)/
reg.exec('abc123')
// ["abc", index: 0, input: "abc123", groups: undefined]

let reg2 = /abc(?=1234)/
reg2.exec('abc123')
// null

let reg3 = /abc(?=12)/
reg3.exec('abc123')
// ["abc", index: 0, input: "abc123", groups: undefined]

先按照正则的字面意思理解,/abc(?=123)/期望的匹配为即匹配abc,且abc后面的字符串能够满足括号的匹配规则,注意的是括号里面可以为其他正则表达式,并不是说abc后面只能包含123,而是后面可以满足括号的匹配则为断言成功。

在reg2的匹配过程中,因为abc后面的字符串不满足括号的匹配规则,所以断言失败,执行匹配也失败了。

在这几个例子里还没有体现出我们概念里说的意思,重温一下正向先行断言的概念

(?=pattern) 正向先行断言:代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配pattern

概念里说的意思断言是在字符串中寻找符合断言的一个位置

举例说明:

let reg = /(?=abc).*/
reg.exec('123abc123')
// ["abc123", index: 3, input: "123abc123", groups: undefined]

先分析正则表达式,在满足abc匹配条件的位置后面匹配任意字符。在这个例子里,存在abc满足断言的匹配规则,但是为什么匹配到的是abc123?

在这里就回到我们的标题,零宽断言,零宽的意思就是执行断言是不会消耗我们正则表达式在匹配过程中的字符串,并且,断言是在帮我们确定符合断言匹配规则的位置。所以,(?=abc)会帮我们确定一个断言成功的位置,即3和a之间的位置,然后在这个断言成功的位置开始执行匹配(.*)。

let reg = /(?=abc)\d+/
reg.exec('123abc123')
// null

在上面的例子中,虽然abc的断言成功,但是断言只是帮我们确定一个位置,然后再执行\d+匹配规则,因为断言是不会消耗字符串,所以实际上以abc123去和\d+匹配,最后匹配结果为null。

基于此我们可以使用断言帮我们从一开始检索整个字符串是否满足某些规则,有助于提升匹配效率。

如下例子,我们可以使用断言从一开始判断整个字符串是否全部由数字组成,如果断言失败,则不执行匹配,这对于我们应用于表单校验非常有助于提升效率。

reg = /(?=^\d+$)\d+/
reg.exec('123456')  // 123456
reg.exec('123456a') // null

另外没有介绍到的三种模式也是大同小异,在这里也就不重复赘述。但是两种后行断言可能会存在兼容性问题,后行断言应该是ES2018新增的规范。

零宽断言的重点是要理解“零宽”以及“位置”这两个点。

最后总结一下:正则表达式是一门非常实用的工具语言,基本上只要学习了就能够对于我们实际开发中产生帮助,平时某些开发工具中也可以使用正则表达式去检索某些文档,对于提升效率真的是帮助非常大。