先记住这个答案
区别在于量词先试多少个字符。贪婪量词先尽可能多地吃,后面的表达式匹配不上时,再一个个吐回去重试;懒惰量词是在量词后面加一个 ?,比如 *?、+?,它先吃最少的,不够再一个个多吃。在 say "one" then "two" 里,/".+"/ 会匹配到 "one" then "two",/".+?"/ 只匹配 "one"。不过实际项目里更可靠的写法往往是 /"[^"]+"/,直接规定引号之间不能再有引号。
*、+、?、{m,n}默认贪婪- 量词后加
?变懒惰,先少后多 - 懒惰不等于最短,最左边的起点优先
- 能用
[^"]+限定字符就别靠懒惰
用引号匹配看两者的差别
最常见的需求是把引号里的内容取出来。一段文本里有两对引号时,差别就出来了。
const text = 'say "one" then "two"'
console.log(text.match(/".+"/)[0]) // 贪婪:一路吃到最后一个引号
console.log(text.match(/".+?"/)[0]) // 懒惰:遇到第一个能收尾的引号就停
console.log(text.match(/"[^"]+"/g).join('|')) // 排除引号,意图最明确查看输出与解释
"one" then "two"
"one"
"one"|"two"贪婪版本出人意料地把中间的 then 也吞了进去。很多人写爬虫或者解析模板时遇到的「匹配多了」,基本都是这个原因。
第三种写法用的是排除型字符类 [^"],意思是「除了引号以外的任意字符」。它从根上规定了引号中间不能再出现引号,所以不需要依赖贪婪还是懒惰,意图也写得最清楚。
背后是回溯在工作
要理解为什么贪婪会匹配到最后一个引号,得知道正则引擎是怎么试的。JavaScript 的正则引擎是回溯型的:遇到有多种吃法的地方就记下一个选择点,先按一种吃法往下走,走不通再退回来换一种。
<.+> 怎么匹配JavaScriptconst html = '<a><b>'
// 贪婪:.+ 先吃到字符串末尾,发现后面还需要 >,再一个个往回吐
console.log(html.match(/<.+>/)[0])
// 懒惰:.+? 先只吃 1 个字符 a,后面正好是 >,立刻成功
console.log(html.match(/<.+?>/)[0])查看输出与解释
<a><b>
<a>贪婪的过程是:< 匹配第一个字符,.+ 一口气吃掉 a><b> 直到末尾。接着表达式还需要一个 >,但已经没有字符了,于是 .+ 吐出最后一个 >,这时 > 能匹配上,整体成功,结果就是整串。
懒惰的过程反过来:.+? 至少要吃一个字符,先吃 a,然后检查后面是不是 >,是的,成功,结果是 <a>。
所以两者的结果都满足整条正则,只是试探顺序不同,先找到的答案不同。量词本身的上下限不变,+? 也至少要吃一个字符。
- 懒惰不等于最短匹配
'aab'.match(/a+?b/)得到的是aab,而不是更短的ab。原因是引擎先从下标 0 开始尝试,a+?先吃一个a,后面是a不是b,于是多吃一个,后面是b,成功。从下标 0 就已经能成功了,引擎不会再去下标 1 找更短的。匹配的起点总是最左边能成功的位置,懒惰只影响从这个起点往后吃多少。
容易答错的地方
- 以为懒惰量词只会匹配一个字符
- 懒惰只是从最少开始试。后面的表达式需要它多吃时,它会一直往后扩张。
/".+?"/遇到"hello world"一样能吃下中间的 11 个字符,它停下的条件是后面的"能匹配上,而不是吃够一个就停。 - 以为懒惰一定比贪婪快
- 两者只是试探顺序不同,哪个快取决于文本。目标内容很短、离起点近时懒惰试得少;目标内容一直延伸到文本末尾时,贪婪一口吃到底再少量回吐,反而比懒惰一个个试要快。真正影响性能的是有没有大量回溯,而不是加不加
?。
面试官还会怎么问?
什么是灾难性回溯?
正则里有嵌套的量词,而且内外层能匹配同样的字符时,比如 /(a+)+b/,面对一串 a 后面没有 b 的输入,引擎要把「这串 a 怎么分给内外两层」的所有分法都试一遍,尝试次数随长度指数增长,几十个字符就能让页面卡死。这类问题叫 ReDoS。解决办法是消除嵌套量词的重叠,改成 /a+b/,而不是把 + 改成 +?,懒惰一样要试完所有分法。
.*? 能用来解析 HTML 吗?
简单提取可以,比如从一段固定格式的片段里取某个标签的文本。但 HTML 有嵌套、属性里可能有 >、有注释和转义,/<div>.*?<\/div>/ 遇到嵌套的 <div> 会在第一个 </div> 就停下,拿到半截内容。浏览器里用 DOMParser,Node.js 里用专门的 HTML 解析库。
{2,4}? 这种写法是什么意思?
区间量词也可以变懒惰。/a{2,4}/ 对 aaaa 会匹配 4 个,/a{2,4}?/ 先只匹配 2 个,后面没有其他要求就直接成功。? 本身也是量词,?? 表示「0 个或 1 个,优先 0 个」,比较少见,但面试偶尔会问。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。