关于正则位置匹配(断言)的技巧

正则位置匹配

先了解下如下几个概念

  • 零宽:只匹配位置,在匹配过程当中,不占用字符,因此被称为零宽git

  • 先行:正则引擎在扫描字符的时候,从左往右扫描,匹配扫描指针未扫描过的字符,先于指针,故称先行github

  • 后行:匹配指针已扫描过的字符,后于指针到达该字符,故称后行,即产生回溯正则表达式

  • 正向:即匹配括号中的表达式api

  • 负向:不匹配括号中的表达式ui

es5 就支持了先行断言google

es2018 才支持后行断言url

零宽正向先行断言,又称正向向前查找(positive lookhead)

注意: .在正则里面表明匹配除换行符,回车符等少数空白字符以外的任何字符,匹配其时须要转义

(?=pattern):某位置后面紧接着的字符序列要匹配 patternes5

例:spa

`sinM.`.match(/sin(?=M\.)/g); // ["sin"]
`M.sin`.match(/sin(?=M\.)/g); // null
复制代码

第一个 sin 会匹配,由于他后面有 pattern指针

零宽负向先行断言,又称负向向前查找(negative lookhead)

(?!pattern):某位置后面紧接着的字符序列不能匹配 pattern

例:

`M.sin`.match(/sin(?!M\.)/g); // ["sin"]
`sinM.`.match(/sin(?!M\.)/g); // null
复制代码

第一个 sin 会匹配,由于他后面没有 pattern

零宽正向后行断言,又称正向向后查找(positive lookbehind)

(?<=pattern):某位置前面紧接着的字符序列要匹配 pattern

例:

'sinM.'.match(/(?<=M\.)sin/g); // null
'M.sin'.match(/(?<=M\.)sin/g); // ["sin"]
复制代码

第二个 sin 会匹配,由于它前面有 pattern

零宽负向后行断言,又称负向向后查找(negative lookbehind)

(?<!pattern):某位置前面紧接着的字符序列不能匹配 pattern

例:

'sinM.'.match(/(?<!M\.)sin/g); // ["sin"]
'M.sin'.match(/(?<!M\.)sin/g); // null
复制代码

第一个 sin 会匹配,由于它前面没有 pattern


来看个实际的例子,把4+6*sqrt(5)*Math.sqrt(5)转换成能够经过eval或者new Function()得到实际结果的字符串

这个可使用负向后行断言,即替换前面不紧接 Math.的 sqrt 字符串序列

let s = `4+6*sqrt(5)*Math.sqrt(5)`.replace(/(?<!Math\.)sqrt/g, func => `Math.${func}`);
eval(s); // 34
复制代码

第二个例子: 匹配 url 后面的路径

'https://www.google.com/v3/api/getUser?user=panghu'.match(/(?<=\.\w*(?=\/)).*/);
复制代码

第三个例子:替换字符串中 img 标签的 width 为 100%

'<img id = "23" style="width:999x;"/><img id = "23" style="width:999x;"/>'.replace(
  /(?<=(<img[\s\S]*width:\s*))[^("\/);]*/gm,
  '100%'
);
复制代码

匹配 sin

'M.sin'.match(/(?<=M\.)sin/g); // ["sin"]
`M.sin`.match(/sin(?!M\.)/g); // ["sin"]
复制代码

这两种方法均可以实现一样的效果,但我我的更喜欢使用第一种方法,它的写法更符合人的直接思惟习惯

在全局匹配修饰符 g 做用下正则 test 方法出现的“怪异”结果

先看下面两行代码的运行结果

let reg = /js/g;
reg.test('js'); //before: lastIndex:0, after: lastIndex:2
reg.test('js'); //before: lastIndex:2, after: lastIndex:0
reg.test('js'); //before: lastIndex:0, after: lastIndex:2
复制代码

若是你的答案是三个 true 的话,那就错了 答案实际上是 true、false、true,这就是所谓的怪异现象

为何?答: RegExp 对象有个 lastIndex 属性,它的初始值是 0, 当不使用 g 修饰符修饰时,每次执行 test 方法以后它都会自动置 0 而使用 g 修饰符时,每次执行 test 方法的时候,它都是从索引值为 lastIndex 的位置开始匹配,lastIndex 为匹配到的字符序列下一个索引值。只有当匹配失败之后才会将 lastIndex 置为 0

例:上述例子中的第一个 test 方法执行以前,lastIndex 值为 0,执行以后 lastIndex 值为 2,因而当第二次执行 test 方法时,从字符串索引值为 2 处开始匹配,显然会匹配失败,因此第三次匹配时又会匹配成功

匹配含 class 为 root 的标签(不考虑特殊状况), 如<div class="root">

这里能够涉及到的知识点有:贪婪/非贪婪匹配模式匹配回溯及其消除分组反向引用

基础版:只匹配双引号包裹的 class

`<div class="root"><span class="root"></span><i class='root'></i></div>`.match(/<[^>]*class="root".*?>/g);
// ["<div class="root">", "<span class="root">"]
复制代码

模式匹配[^>]表示匹配除[^]里面的全部字符,这里就是匹配除>外的全部字符 注意先后都须要非贪婪匹配符号?不然只有前面的,它会贪婪的吃掉 div;只有后面的,它会贪婪的吃掉 span

完整版:单双引号包裹的 class 均可以匹配

`<div class="root"><span class="root"></span><i class='root'></i></div>`.match(/<[^>]*class=("root"|'root').*?>/g);
// ["<div class="root">", "<span class="root">", "<i class='root'>"]
复制代码

这里若是不使用[^>]而使用.*就会出现下面这种匹配结果,不是咱们想要的

["<div class="root">", "<span class="root">", "</span><i class='root'>"]

进阶版:使用分组引用消除难看的("root"|'root'),再消除.*?回溯

`<div class="root"><span class="root"></span><i class='root'></i></div>`.match(/<[^>]*class=("|')root\1[^>]*>/g);
// ["<div class="root">", "<span class="root">", "<i class='root'>"]
复制代码

\1表示引用前面的第一个分组结果,即("|')的匹配结果,这样就能保证单引号配对单引号,双引号匹配双引号

[^>]*代替.*?能够消除使用*?引起的回溯,由于*是尽量多的匹配,而?是尽量少的匹配

回顾开头,我所说的特殊状况就是标签的属性值不能含有>,由于为了消除回溯使用的[^>]含有字符>,这部分其实可使用其余正则代替,让它在消除回溯的状况下能够匹配特殊状况

若是你们对匹配含 class 为 root 的标签这部分涉及的知识点感兴趣,能够在底下评论,我到时候再仔细讲

若是你喜欢这篇文章的话,麻烦点个⭐原文地址资瓷下

参考:

JavaScript 权威指南(第 6 版)

Javascript 正则表达式迷你书

以上若有错误,欢迎指正

相关文章
相关标签/搜索