正则位置匹配
先了解下以下几个概念
零宽: 只匹配位置, 在匹配过程中, 不占用字符, 所以被称为零宽
先行: 正则引擎在扫描字符的时候, 从左往右扫描, 匹配扫描指针未扫描过的字符, 先于指针, 故称先行
后行: 匹配指针已扫描过的字符, 后于指针到达该字符, 故称后行, 即产生回溯
正向: 即匹配括号中的表达式
负向: 不匹配括号中的表达式
在 1999 年 ECMAScript3 标准就支持了先行断言
直到 2009 年 ECMAScript5 标准才支持后行断言
零宽正向先行断言, 又称正向向前查找(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
复制代码
匹配 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 的标签这部分涉及的知识点感兴趣, 可以在底下评论, 我到时候再仔细讲
来源: https://juejin.im/post/5b583fede51d4516e91f9e2f