1、定义
1.1 什么是断言?
断言用于查找某些内容或内容所在的位置,该内容或内容所在位置应满足一定的条件。
那么什么叫零宽断言呢?零宽又是什么意思?
1.2 零宽断言
通俗的讲,零宽断言的字面意思是匹配宽度为零的断言。
所谓匹配的宽度为零,就是说这种断言在工作时:
只作为匹配的判断条件
实际不匹配任何的结果
也不捕获任何的内容
这三个点,既展示了零宽断言的定义,也说明了零宽断言的两个特性:非捕获性和不吃字符
这两个特性我放在下个章节来讲。
2、零宽断言表达式与案例
因为后面有一些案例代码,为了能让大家更清楚的理解代码,所以这个章节先讲一下零宽断言表达式
2.1 表达式
相信你看这个表格就能理解十之八九了,我这里举个小例子
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
。它规定了正则表达式下一次匹配的起始位置。
如果我们使用正则的方法如test
,exec
等方法,就能清楚的看到 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
,只匹配了h
和e
,这能更明显的体现不吃字符的特性。
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、结尾
创作不易,如果这篇文章帮助到了你,请动动手指点个赞再走吧~
作者:晴天同学