正则位置匹配
先了解下以下几个概念
-
零宽:只匹配位置,在匹配过程中,不占用字符,所以被称为零宽
-
先行:正则引擎在扫描字符的时候,从左往右扫描,匹配扫描指针未扫描过的字符,先于指针,故称先行
-
后行:匹配指针已扫描过的字符,后于指针到达该字符,故称后行,即产生回溯
-
正向:即匹配括号中的表达式
-
负向:不匹配括号中的表达式
es5 就支持了先行断言
es2018 才支持后行断言言
零宽正向先行断言,又称正向向前查找(positive lookhead)
注意:
.
在正则里面代表匹配除换行符,回车符等少数空白字符之外的任何字符,匹配其时需要转义
(?=pattern):某位置后面紧接着的字符序列要匹配 pattern
例:
`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 正则表达式迷你书
以上如有错误,欢迎指正
写的不错,star一下,期待后续~~
来自✨ Node.js开源项目精选✨
@vendar 谢谢😀
贴一篇我写的相关文章:http://fxck.it/post/50558232873
大家可以参考铁路图学习理解 http://regexper.cn/ (汉化的不准确的大家可以提一下)