断言与回溯
断言(Assertion)和回溯(Backtracking)是正则中"分水岭"级别的内容。会用断言,意味着你能写出"在某个条件下匹配"的精准模式;理解回溯,意味着你能解释为什么一行正则会让页面卡死。这两块都是高级前端面试的高频考点。
断言
断言用来匹配一个位置,而不是字符。它要求这个位置满足某种条件,但不消耗任何字符。先看一下分类:
| 类型 | 写法 | 含义 |
|---|---|---|
| 正向先行断言 | (?=...) | 右边必须出现 ... |
| 负向先行断言 | (?!...) | 右边不能出现 ... |
| 正向后行断言 | (?<=...) | 左边必须出现 ... |
| 负向后行断言 | (?<!...) | 左边不能出现 ... |
注意"先行/后行"对应"右边/左边","正/负"对应"必须出现/不能出现"。这四个组合是面试常考的口诀。
记忆技巧:
- lookahead = 看前方 = 右边
- lookbehind = 看后方 = 左边
- positive = 必须满足
- negative = 不能满足
先行断言
正向先行 (?=...)
匹配一个位置,要求其右边是指定模式:
// 匹配后面跟着数字的字母
"a1 b2 c3".match(/[a-z](?=\d)/g); // ['a', 'b', 'c']
// 注意:数字本身没有被消耗
"a1 b2 c3".match(/[a-z]\d/g); // ['a1', 'b2', 'c3']
经典应用:在金额中加千分位
"123456789".replace(/\B(?=(\d{3})+$)/g, ",");
// '123,456,789'
原理分析:
\B:非单词边界,确保不是开头位置(?=(\d{3})+$):右边必须是 3 的倍数个数字直到结尾g:所有满足条件的位置都替换
负向先行 (?!...)
匹配一个位置,要求其右边不是指定模式:
// 匹配不以数字开头的单词
"abc 1abc xyz".match(/\b(?!\d)\w+/g); // ['abc', 'xyz']
// 排除某个词
const re = /\b(?!cat\b)\w+/g;
"cat dog category".match(re); // ['dog', 'category']
// 注意 cat 仍然在 category 里出现,但负向断言只看完整词边界
后行断言
ES2018 引入,浏览器支持已经较好。
正向后行 (?<=...)
匹配一个位置,要求其左边是指定模式:
// 匹配 ¥ 后面的数字
"价格 ¥100,数量 ¥50".match(/(?<=¥)\d+/g); // ['100', '50']
负向后行 (?<!...)
匹配一个位置,要求其左边不是指定模式:
// 匹配不在 ¥ 后面的数字
"¥100 和 50".match(/(?<!¥)\b\d+\b/g); // ['50']
后行断言的经典场景是"基于上下文做条件匹配",比如把
foo替换为bar,但只在import语句里:
"import foo;\nfoo();".replace(/(?<=import\s)foo/g, "bar");
// 'import bar;\nfoo();'
断言的"不消耗"特性
断言最关键的特点是不消耗字符。这意味着同一个字符可以被多个断言同时检查:
// 匹配既有数字又有大写字母又有特殊字符的位置(密码强度示例)
const re = /^(?=.*\d)(?=.*[A-Z])(?=.*[!@#$]).+$/;
re.test("aB3!"); // true
re.test("abc123"); // false,缺少大写和特殊字符
每个 (?=.*...) 都从头开始匹配,互不影响,最终要求整个字符串同时满足三个条件。这是断言最常见的组合用法。