先记住这个答案
前瞻和后顾都叫环视断言,作用是在某个位置检查上下文,但不把上下文算进匹配结果。前瞻 (?=...) 检查右边,后顾 (?<=...) 检查左边;把 = 换成 ! 就是反过来的「不能是」。比如 /(?<=\$)\d+/ 匹配美元符号后面的数字,结果里只有数字,没有 $。它们最常见的用途是提取「某个前缀后面的内容」和「在满足条件的位置插入字符」。
- 前瞻看右边,后顾看左边,都不进
match[0] =表示必须是,!表示不能是- 多个前瞻叠在开头,等于多个条件同时成立
- 后顾写在要匹配内容的左边,前瞻写在右边
用前瞻写千分位格式化
面试最常出现的前瞻题就是给数字加千分位逗号。这里用到的不是「匹配数字」,而是「找到应该插逗号的位置」。
const format = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
console.log(format(1234567)) // 1,234,567
console.log(format(1000)) // 1,000
console.log(format(999)) // 999,不需要逗号查看输出与解释
1,234,567
1,000
999这条正则没有消费任何数字,每次匹配到的都是一个空位置,replace 就往这个位置塞一个逗号。拆开看:\B 要求这里不是单词边界,排除了数字最左边的位置,避免出现 ,123。(?=(\d{3})+(?!\d)) 要求右边是 3 的整数倍个数字,而且这串数字之后不能再有数字,也就是一直数到末尾。
比如在 1234567 里,1 和 2 之间的位置右边是 234567,正好 6 个数字,满足条件。2 和 3 之间右边是 34567,5 个,不满足。所以逗号只插在 1 后面和 4 后面。
前瞻不吃字符的好处在这里体现出来了:检查完右边之后,读取位置还在原地,下一个位置可以继续检查同一串数字。
用后顾提取前缀后面的内容
后顾解决的是另一类需求:我要的内容左边必须是某个东西,但我不想要这个东西。以前没有后顾,只能把前缀一起匹配出来再用捕获组取值。
const text = '原价 $120,折后 $99,运费 15 元'
// 左边必须是 $
console.log(text.match(/(?<=\$)\d+/g).join(','))
// 左边不能是 $,\b 保证从数字开头算起
console.log(text.match(/(?<!\$)\b\d+/g).join(','))查看输出与解释
120,99
15第一条正则在每个数字前检查左边是不是 $,是就开始匹配。结果里没有 $,不用再手动 slice。
第二条用了负向后顾。只写 (?<!\$)\d+ 会出问题:120 的 1 前面是 $ 被排除了,但引擎会接着从 2 开始试,2 前面是 1 不是 $,于是匹配出 20。加上 \b 要求从数字的开头算起,才能只拿到 15。负向断言经常需要这样补一个边界。
JavaScript 的后顾还有个比很多语言宽松的地方:里面可以写不定长的内容,比如 (?<=\$\s*)。Python 标准库的 re 模块就要求后顾里的内容长度固定。
- 多个前瞻叠在一起就是「同时满足」
- 密码规则「至少 8 位,包含数字和小写字母」可以写成
/^(?=.*\d)(?=.*[a-z]).{8,}$/。两个前瞻都站在开头这个位置检查,都不移动读取位置,所以相当于在同一个起点连续检查两个条件,最后再由.{8,}真正匹配整串。不过实际项目里更推荐拆成几条独立规则,这样能告诉用户具体是哪一条没满足。
容易答错的地方
- 把前瞻写在要匹配内容的左边
- 想匹配
$后面的数字,写成/(?=\$)\d+/是不行的。前瞻要求当前位置右边是$,紧接着\d+又要求同一个位置右边是数字,一个字符不可能既是$又是数字,所以永远匹配不上。检查左边要用后顾,前瞻只能放在要匹配内容的右边。 - 以为断言的内容会出现在结果里
- 断言只检查不消费,
match[0]里只有断言之外那部分表达式匹配到的字符。但断言里如果写了捕获组,比如(?=(\d{3})+),捕获组的内容还是会出现在match[1]里。不需要读的时候写成(?:...)。
面试官还会怎么问?
老浏览器不支持后顾,怎么降级?
后顾断言是 ES2018 加入的,Chrome 很早就支持,但 Safari 直到 16.4 才支持,更老的 iOS WebView 里写后顾会直接抛 SyntaxError,整个脚本都跑不起来。降级方法是把前缀放进匹配里,再用捕获组取值,比如 /\$(\d+)/g 配合 matchAll 取 m[1]。
负向前瞻 (?!...) 有什么典型用法?
排除某些情况。比如匹配不以 .min.js 结尾的 js 文件名,可以写 /^(?!.*\.min\.js$).*\.js$/:先在开头检查整串里不会出现以 .min.js 结尾的情况,再正常匹配 .js 结尾。
前瞻断言会影响性能吗?
断言本身开销不大,问题出在断言里写了 .* 这种会扫完整串的表达式,又放在会被反复尝试的位置。比如千分位那条正则,每个位置都要往右扫到末尾,对超长数字串就是 O(n²)。平时格式化金额没问题,处理大文本时要留意。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。