首页>>前端>>JavaScript->JavaScript正则之零宽断言详解

JavaScript正则之零宽断言详解

时间:2023-11-29 本站 点击:2

1、定义

1.1 什么是断言?

断言用于查找某些内容或内容所在的位置,该内容或内容所在位置应满足一定的条件。

那么什么叫零宽断言呢?零宽又是什么意思?

1.2 零宽断言

通俗的讲,零宽断言的字面意思是匹配宽度为零的断言。

所谓匹配的宽度为零,就是说这种断言在工作时:

只作为匹配的判断条件

实际不匹配任何的结果

也不捕获任何的内容

这三个点,既展示了零宽断言的定义,也说明了零宽断言的两个特性:非捕获性和不吃字符

这两个特性我放在下个章节来讲。

2、零宽断言表达式与案例

因为后面有一些案例代码,为了能让大家更清楚的理解代码,所以这个章节先讲一下零宽断言表达式

2.1 表达式

表达式名称描述(?=exp)正向(正预测)先行断言匹配某个位置,该位置的后面的内容应匹配表达式 exp(?!exp)负向(负预测)先行断言匹配某个位置,该位置的后面的内容不匹配表达式 exp(?<=exp)正向(正回顾)后发断言匹配某个位置,该位置的前面的内容应匹配表达式 exp(JS不支持)(?<!exp)负向(负回顾)后发断言匹配某个位置,该位置的前面的内容不匹配表达式 exp(JS不支持)

相信你看这个表格就能理解十之八九了,我这里举个小例子

2.2 案例解析

现在有一个需求是这样的:

在字符串 '北京市(朝阳区)(西城区)(海淀区)' 中,取出没有被()包裹的字符北京市

这怎么做呢?

要求是取出没有被()包裹的字段,那这个字段的后面紧跟着的一定是(,就是这么简单。

那我可以写一个零宽断言表达式 var reg = /.*?(?=\()/ 来看看能不能满足需求:

console.log(reg.exec('北京市(朝阳区)(西城区)(海淀区)'))//["北京市",index:0,input:"北京市(朝阳区)(西城区)(海淀区)",groups:undefined]console.log('北京市(朝阳区)(西城区)(海淀区)'.match(reg))//["北京市",index:0,input:"北京市(朝阳区)(西城区)(海淀区)",groups:undefined]

简单做个解释,这个 reg可以看作两部分,分别是.*?(?=\()

假设你已经知道了在正则表达式当中:

. 代表匹配除了换行和行结束符的任意字符

*代表匹配任意次数,保证我们得到的是北京市而不是这个单字

?代表对多个连续的值,要么匹配0次,要么匹配1次,最多匹配一次

那么前一部分就可以匹配给定字符中包含的所有字符,而后一部分零宽断言则给匹配添加了条件,要求匹配的内容后面跟着(,于是我们就能得到想要的结果

2.3 贪婪模式与非贪婪模式

你可能有疑问,如果我正则写成这个样子/.*(?=\()/,为什么结果就不对?

console.log('北京市(朝阳区)(西城区)(海淀区)'.match(/.*(?=\()/))//["北京市(朝阳区)(西城区)",index:0,input:"北京市(朝阳区)(西城区)(海淀区)",groups:undefined]

我首先说明,这所以在这里加入贪婪模式与非贪婪模式,是因为上面的案例是正向(正预测)先行断言的案例 而网上有些人说正向零宽断言是从右向左执行的,我就不批判了,只希望大家能够擦亮眼睛。

那么为什么少了一个问号,得出了全然不同的结果呢?

上面说了,?在这个表达式里,表示要么不匹配,要么最多匹配一次,这就是非贪婪模式

而不添加?,表达式以贪婪模式运行。什么是贪婪模式?就是尽可能多的去匹配,匹配到了还去匹配,贪得无厌,一直匹配到字符串的末尾

因为字符串中有三个(,贪婪模式下会一直匹配到字符串结尾,就会得到北京市(朝阳区)(西城区)这个结果

3、零宽断言的特性

第一章已经说了零宽断言定义和它的特性,这里来详细讲讲它的这两个特性:非捕获特性和不吃字符特性

3.1 非捕获特性

这是零宽断言的一个基本特性,非捕获特性

我假设你知道在正则表达式中可以使用()来进行分组取值,一个()就是一个分组。 所以我这里举一个使用分组取值替换字符的例子:

varstr='Hello,Wrld!';varreg=/(W)/g;console.log(str.replace(reg,"$1o"))//Hello,world!

上面的代码,我使用分组来获取W并将它成功替换成正确的字符Hello, world! 现在改一下代码,看看会怎样:

varstr='Hello,Wrld!';varreg=/W(?=r)/g;console.log(str.match(reg))//["W"]//注意看使用match方法返回了正确的结果,那么我能替换成功吗console.log(str.replace(reg,"$1o"))//Hello,$1orld!

很显然,虽然 str.match 方法返回了正确的结果,但是替换并没有实现想要的效果 并且这个结果和使用非捕获性分组是一致的:

varstr='Hello,Wrld!';varreg=/(?:W)/g;console.log(str.replace(reg,"$1o"))//Hello,$1orld!

所以我们得出结论:

虽然第二个代码片段也使用了()进行分组,但因为这个分组使用的是零宽断言的表达式,正则并没有返回针对$1的引用,所以替换是不能成功的。

由此,这个例子很好的证明了零宽断言具有非捕获的特性。

3.2 不吃字符特性

零宽断言的另一个特性,是不吃字符。

这里的不吃字符不是寻常意义上的吃,而是说零宽断言本身不占据查找位置,它不算是要被查找的内容,因此,正则表达式并不匹配零宽断言本身。

这里我们先引申一个概念。

这个概念,是正则对象的一个属性,叫做 lastIndex。它规定了正则表达式下一次匹配的起始位置。

如果我们使用正则的方法如testexec等方法,就能清楚的看到 lastIndex的影响:

varreg=/([a-z])/g;vararr=["a","b","c","d","e"];for(vari=0;i<arr.length;i++){console.log(reg.exec(arr[i]))}

这段代码的返回结果,相信很多人都会判断错误,我直接把结果放在下面

["a", "a", index: 0, input: "a", groups: undefined]

null ["c", "c", index: 0, input: "c", groups: undefined] null ["e", "e",index: 0, input: "e", groups: undefined]

是不是惊掉了一地下巴?

这就是lastIndex在搞怪,简单的说,当正则全局匹配成功时,lastIndex就指向了匹配成功的字符索引,再次匹配时,则从lastIndex位置继续向后匹配

注意数组中每一项的长度都是1,如果lastIndex的值大于等于1,自然匹配的结果就是null; 没有匹配到结果,lastIndex的值就成了默认值0,下一次的匹配就是非 null 的结果了。

所以我们得出结论,lastIndex随着匹配结果的变化而变化

下面我们使用零宽断言的方式来试试。

varreg=/(?=[a-z])/g;vararr=["a","b","c","d","e"];for(vari=0;i<arr.length;i++){console.log(reg.exec(arr[i]))}

这个结果不知道你能猜出来吗?

["", index: 0, input: "a", groups: undefined]

["", index: 0, input: "b", groups: undefined] ["", index: 0, input: "c", groups: undefined] ["", index: 0, input: "d", groups: undefined] ["", index: 0, input: "e", groups: undefined]

而因为正则表达式不匹配零宽断言本身,因此零宽断言不会改变lastIndex的值,所以它返回了空值“”,却没有完全返回空(null)。

如果你依然不能理解,尝试看看下面这段代码:

varstr='hello,thisishierry,andthatishi,thereishandsome'varreg=/h(?!i)e/console.log(str.match(reg))

这段代码十分简单,结果只会匹配一项

["he", index: 0, input: "hello, this is hierry, and that is hi, there is handsome", groups: undefined]

很明显,reg 匹配的时候越过了表达式中间的i,只匹配了he,这能更明显的体现不吃字符的特性。

4、零宽断言的四种类型

通常,零宽断言依据断言的方式被分为四类,分别是:

零宽度正向先行断言(也叫正预测先行断言)

零宽度负向先行断言(也叫负预测先行断言)

零宽度正向后发断言(也叫正回顾后发断言)

零宽度负向后发断言(也叫负回顾后发断言)

在 javascript 这门语言中,只支持先行断言,不支持后发断言

4.1 正向断言和负向断言有什么区别呢?

正向断言是当某个位置前面或者后面的内容要匹配表达式 exp,才会返回非空的结果

负向断言是当某个位置前面或者后面的内容不匹配表达式 exp,才会返回非空的结果

4.2 先行断言和后发断言的区别?

先行断言是用某个位置后面的内容与表达式 exp 进行匹配

后发断言是用某个位置前面的内容与表达式 exp 进行匹配

4.3 零宽度正向先行断言

例子一,是我在面试题里找到的

衣服38元、鞋子62元、剪发15元、吃饭45元、打车30元、冰激凌10元,对花费的金钱求和?

答案:/\d.?\d(?=[元块])/g

vars='衣服38元、鞋子62元、剪发15元、吃饭45块、打车30元、冰激凌10元'varr=/\d.?\d(?=[元块])/gvarsum=s.match(r).reduce(function(prev,cur){returnprev*1+cur*1;})console.log(sum);//200

例子二

'i like eating 、sleeping、 see movies and ' 中拿到以 ing 结尾的动作单词

答案:/\w+(?=ing)/g

vars='ilikeeating、sleeping、seemoviesand'varr=/\w+(?=ing)/gconsole.log(s.match(r))//["eat","sleep"]

这两个例子比较简单,就不坐过多解释了。

4.4 零宽度负向先行断言

举个例子,也是我在面试题中找到的

测试一个文件是否是.css后缀,但又不能是.min.css,如:

test('a.min.css'); // false test('b.css'); // true test('c.mining.css'); // true

答案:/^(?!.*\.min\.css$).+\.css$

这里先对以任意字符开头结尾为.min.css 的文件名做了排除,在剩余的文件名中查找以.css为结尾的文件名

console.log('北京市(朝阳区)(西城区)(海淀区)'.match(/.*(?=\()/))//["北京市(朝阳区)(西城区)",index:0,input:"北京市(朝阳区)(西城区)(海淀区)",groups:undefined]0

5、结尾

创作不易,如果这篇文章帮助到了你,请动动手指点个赞再走吧~

作者:晴天同学


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/684.html