js中的正则

正则表达式

正则表达式是一种特殊的字符串模式,用于匹配一组字符串,就好比用模具做产品,而正则就是这个模具,定义一种规则去匹配符合规则的字符。

创建方式

  • 使用 RegExp 对象创建
    第一个参数就是我们的模式字符串,第二个参数可选,模式修饰符,”i”表示忽略大小写,”g”表示全局匹配
1
const reg = new RegExp('study', 'ig')
  • 直接声明
1
const reg = /study/ig

正则表达式的方法

RegExp.test

1
正则表达式对象名.test(“字符串”)

用于检测提供的目标字符串是否包含正则表达式的匹配内容,如果包含返回 true,否则返回 false

RegExp.exec

1
正则表达式对象名.exec(“字符串”)

用于在字符串中查找指定正则表达式,如果 exec()方法执行成功,则返回包含该查找字符串的相关信息数组。如果执行失败,则返回 null。

元字符

  • {} 表示括号前面的一个字符连续出现的次数

    {m}:表示括号前面的一个字符只能连续出现 m 次

    {m,}:表示至少出现 m 次,即 m~无限次

    {m,n}:表示至少出现 m 次,最多出现 n 次,即 m~n 次

  • [] 表示范围

    [\u4e00-\u9fa5]:表示中文的范围

    [^\u4e00-\u9fa5]:表示非中文

    [^a-za-z]:表示非字母

    [^0-9]:表示非数字

    [^ ]:表示非空格

  • () 表示组

  • * 等同于{0,}表示 0~无限次

  • + 等同于{1,} 表示 1~无限次,至少出现 1 次

  • ? 等同于{0,1}表示 0~1 次,最多可出现 1 次,可以不出现

  • ^ 定(断)头:表示必须以^后面的一个字符开头

  • $ 定(断)尾:表示必须以$前面的一个字符结尾

  • . 表示模糊匹配,一个点可以匹配一个任意字符

  • | 表示或者

  • \ 表示转义字符

    \d:表示数字 [0-9]

    \D:表示非数字[^0-9]

    \w:表示字母、数字、下划线 [a-zA-Z0-9_]

    \W:表示非字母、数字、下划线 [^a-za-z0-9_]

    \s:表示空白符

    \S:表示非空白符

    \b:匹配单词边界

优先级

从上往下优先级由高到低

\ 转义
( ) [ ] 圆括号和方括号
* + ? {n} {n,} {n,m} 限定符
^ $ \任何元字符 顺序和位置
|

捕获和非捕获

单纯说到捕获,他的意思是匹配表达式,但捕获通常和分组联系在一起,也就是“捕获组”

捕获组:匹配子表达式的内容,把匹配结果保存到内存中中数字编号或显示命名的组里,以深度优先进行编号,之后可以通过序号或名称来使用这些匹配结果。
什么意思呢?看个例子

1
'123abc'.replace(/(\d+)\D+/g,`[${'$1'}]`)

这个例子意思就是在字符串“123abc”中用另一个字符串替换掉正则匹配到的内容,用来替换的字符串是个字符串模版,里面有个变量是”$1”,代表正则中第一个捕获组的内容也就是正则中(\d+)匹配到的内容,这就是捕获组。

数字编号捕获组

上面简单的例子可以看到(\d+)捕获到的组自动命名了序号 1,
那我们接着来实验

1
2
const regResult = /(0\d{2})-(\d{8})/g.exec('010-12345678');
console.log(regResult);

我们可以看到输出中捕获组长度为 3

1
2
3
4
5
6
7
0: "010-12345678"
1: "010"
2: "12345678"
groups: undefined
index: 0
input: "010-12345678"
length: 3

第 0 组为整个表达式,第一组为第一个小括号匹配到的内容,即(0\d{2}),第二组为第二个小括号匹配到的内容,即(\d{8}),那如果有多个括号嵌套呢?我们试一下

1
2
const regResult = /(0(\d{2}))-(\d{8})/g.exec('010-12345678');
console.log(regResult);

看到输出为:

1
2
3
4
5
6
7
8
0: "010-12345678"
1: "010"
2: "10"
3: "12345678"
groups: undefined
index: 0
input: "010-12345678"
length: 4

可以看出分组是按照小括号从左往右出现的顺序来进行编号的

命名编号捕获组

和数字编号捕获组类似,只不过给捕获组增加了名字,从 ECMAScript 2018 开始才有的这个特性。

1
2
const regResult = /(?<area>0\d{2})-(\d{8})/g.exec('010-12345678');
console.log(regResult);

在正则分组的小括号开头增加?<name>就给这个分组起了名字
上面例子中给第一个分组起了名字叫area,打印结果可以看到:

1
2
3
4
5
6
7
8
9
0: "010-12345678"
1: "010"
2: "12345678"
groups: {
area: "010"
}
index: 0
input: "010-12345678"
length: 3

字段 groups 里多了 area 的字段,也可以直接在正则外使用

1
'010-12345678'.replace(/(?<area>0\d{2})-(\d{8})/g, '$<area>')

把整个字符串中被正则匹配到的地方替换成命名为area的分组

非捕获

非捕获的意思就是不进行捕获,在捕获组的小括号开头增加?:,例子:

1
2
const regResult = /(?:0\d{2})-(\d{8})/g.exec('010-12345678');
console.log(regResult);

结果:

1
2
3
4
5
6
0: "010-12345678"
1: "12345678"
groups: undefined
index: 0
input: "010-12345678"
length: 2

反向引用

通过上面的例子我们已经明白了如何在正则外面使用捕获组,使用$1$<name>来使用。那么反向引用就是在正则内部进行引用。

数字编号组反向引用

1
console.log(/(\d+)\D+\1/g.test('123abc123'));

首先我们看到正则的意思是(1到无穷个数字)1到无穷个非数字\1,这里的\1代表的就是我第一个捕获组捕获到了什么内容那我这里就是什么内容,这就是反向引用。注意:这里是内容,意思就是匹配的结果,而不是拿引用的组的表达式再匹配一次。再一个例子:简单判断成语是不是’ABAC’形式:

1
2
3
const reg = /([\u4e00-\u9fa5])[\u4e00-\u9fa5]\1[\u4e00-\u9fa5]/g;
console.log('人山人海', reg.test('人山人海'));
console.log('海底捞针', reg.test('海底捞针'));

第一个捕获组用\1表示,那第二个捕获组就用\2表示,以此类推

命名编号组反向引用

1
/(?<firstword>[\u4e00-\u9fa5])[\u4e00-\u9fa5]\k<firstword>[\u4e00-\u9fa5]/g.test('人山人海')

\k<name>来引用命名的捕获组,同样的,从 ECMAScript 2018 开始才有的这个特性。

贪婪和非贪婪

贪婪模式

字面意思就是想要更多。当正则表达式中包含能接受重复的限定符时(例如:\*、+、?、{n,m}),在使整个表达式能得到匹配的前提下,匹配尽可能多的字符。注意这里是有前提的,在使整个表达式能得到匹配的前提下,默认是贪婪模式。例如:

1
console.log(/^(\d{1,6}).*$/.exec('123456789'));

整个表达式能够得到匹配后,{1,6}会尽可能多的匹配字符,输出可以看到匹配了”123456”
再看另一个例子:

1
console.log(/^(\d{1,6})\d{8}$/.exec('123456789'));

这个正则为了使整个表达式能够得到匹配,首先得满足\d{8},因此虽然\d{1,6}想获得足够多的字符,但是必须先让整体得到匹配,它虽然贪婪,但是还是得把集体的利益放在首位,这就是贪婪模式。

非贪婪模式(懒惰模式)

当正则表达式中包含能接受重复的限定符时(例如:\*、+、?、{n,m}),在使整个表达式能得到匹配的前提下,匹配尽可能少的字符。注意这里也是有前提的,在使整个表达式能得到匹配的前提下。大家应该能够明白是什么意思了,我们看例子:

1
console.log(/^(\d{1,6}?).*$/.exec('123456789'));

整个表达式能够得到匹配后,{1,6}会尽可能少的匹配字符,是因为这个?,在重复限定符(例如:\*、+、?、{n,m})后面紧跟?代表这个重复限定符为非贪婪模式,输出可以看到匹配了”1”,同样的它会以大局为重,优先使整个表达式能够得到匹配。

1
console.log(/^(\d{1,6}?)\d{3}$/.exec('123456789'));

虽然它不想要很多字符,但是为了大局,它也要了‘123456’

多个的优先级

如果有多个贪婪在一起呢?会出现什么情况?

1
console.log(/^(\d{1,6})(\d{1,6})$/.exec('123456789'));

从结果我们可以看到第一个组捕获到了123456,第二个组捕获到了789

多个贪婪在一起时,如果字符串能满足他们各自最大程度的匹配时,就互不干扰,但如果不能满足时,会根据深度优先原则,也就是从左到右的每一个贪婪量词,优先最大数量的满足,剩余再分配下一个量词匹配。

第一组想要最多,看整体满足的前提下后发现它可以拿最多的 6 个,它就拿了 6 个,第二组想要更多,但是第一组只给他留了 3 个,它就只能拿三个

1
console.log(/^(\d{1,6}?)(\d{1,6}?)$/.exec('123456789'));

非贪婪也类似,从左往右优先满足,第一组想要最少的,但是发现只要一个的话满足不了整体匹配的前提,发现最少得拿 3 个才能满足,所以它拿了 3 个,第二组也不想要这么多,但是第一组给它留了 6 个,那它只能要 6 个

所以虽然他们贪婪或是懒惰,但是他们都以整体利益出发,而且懂礼貌,让先来的先挑。

零宽断言

断言:俗话的断言就是“我断定什么什么”,而正则中的断言,就是说正则可以指明在指定的内容的前面或后面会出现满足指定规则的内容,意思正则也可以像人类那样断定什么什么,比如”ss1aa2bb3”,正则可以用断言找出 aa2 前面有 bb3,也可以找出 aa2 后面有 ss1.

零宽:就是没有宽度,在正则中,断言只是匹配位置,不占字符,也就是说,匹配结果里是不会返回断言本身。

  • 前瞻断言

    • (?=exp) 顺序肯定环视,表示所在位置右侧能够匹配 exp
    • (?!exp) 顺序否定环视,表示所在位置右侧不能匹配 exp
  • 后瞻断言

    • (?<=exp) 逆序肯定环视,表示所在位置左侧能够匹配 exp
    • (?<!exp) 逆序否定环视,表示所在位置左侧不能匹配 exp

顺序肯定环视(正前瞻)

举个例子:

1
console.log(/\w*(?=\.gif)/.exec('hello.gif'));

结果:

1
2
3
4
5
0: "hello"
groups: undefined
index: 0
input: "hello.gif"
length: 1

这个前瞻断言的正则意思就是在某一个位置上,它的左边是\w*,它的右边是.gif,并且这个正则匹配的宽度是\w*的宽度,因为是零宽断言,所以不返回断言之中的内容。
比如这样:

1
console.log(/hello(?=\.gif)\.gif/.test('hello.gif'));

这个例子虽然比较鸡肋,但是可以很好的理解零宽断言,正则其实是/hello\.gif/,但是在hello.中间的位置加了一个断言,断定在我这个位置之后是 gif。我只贡献了一个位置,没有其他的贡献。

上面我们讲的就是前瞻断言中的顺序肯定环视,那么顺序否定环视就很好理解了,就是这个位置之后不是什么什么。

顺序否定环视(负前瞻)

1
console.log(/\w*(?!\.gif)/.exec('hello.gif'));

我们可以看到匹配的是hell,因为他要找到一个位置,这个位置之前是\w*,之后不是.gif,并且\w*还是贪婪模式,那么只好找到lo中间的位置,这样这个位置前面满足\w*,后面是o.gif也满足不是.gif这个条件,而且贪婪的那组已经最多了,再多位置就不满足正则了。

逆序肯定环视(正后瞻)

既然有我断定我在的位置右边有什么什么,那应该也有我断定我在的位置左边有什么什么。值得注意的是,从 ECMAScript 2018 开始才有的这个特性。

1
console.log(/(?<=function\s).*/.exec('function test'));

找到一个位置,这个位置前面是'function\s',后面是'.*',并且这个位置没有贡献任何字符串

逆序否定环视(负后瞻)

相似的,这个意思就是我在的这个位置左边不能有什么什么,相同的,也是从 ECMAScript 2018 开始才有的这个特性。