javascript 零宽断言,看这篇就足够了

1、定义


1.1 什么是断言?

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

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

1.2 零宽断言

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

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

  1. 只作为匹配的判断条件
  2. 实际不匹配任何的结果
  3. 也不捕获任何的内容

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

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


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 非捕获特性

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

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

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

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

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

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

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

所以我们得出结论:

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

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

3.2 不吃字符特性

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

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

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

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

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

var reg = /([a-z])/g;
var arr = ["a", "b", "c", "d", "e"];
for(var i = 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随着匹配结果的变化而变化

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

var reg = /(?=[a-z])/g;  
var arr = ["a", "b", "c", "d", "e"];
for(var i = 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)。

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

var str = 'hello, this is hierry, and that is hi, there is handsome'
var reg = /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.2 零宽度正向先行断言

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

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

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

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

console.log(sum);// 200

例子二

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

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

var s = 'i like eating 、sleeping、 see movies and '
var r = /\w+(?=ing)/g
console.log(s.match(r))   // ["eat", "sleep"]

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

4.2 零宽度负向先行断言

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

测试一个文件是否是.css后缀,但又不能是.min.css,如:
test(‘a.min.css’); // false
test(‘b.css’); // true
test(‘c.mining.css’); // true

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

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

var reg=/^(?!.*\.min\.css$).+\.css$/;

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

5、尾声

创作不易,请动动手指点个赞再走吧

猜你喜欢

转载自blog.csdn.net/zhai_865327/article/details/119005629