先记住这个答案
JavaScript 中 *、+、{n,}、{n,m} 等量词默认是贪婪的:它们会先吃掉尽可能多的字符,再在整体匹配失败时逐步吐出字符回退。在量词后紧跟一个 ?(如 .*?、+?)就变成懒惰模式:先尝试最少的重复次数,只有后续模式无法匹配时才多消费一个字符。因此同一个字符串用 <.*> 可能从第一个 < 一直匹配到最后一个 >,而 <.*?> 只匹配到最近的 >。捕获组的结果也随之不同,因为组内贪婪部分会吞掉本应属于后续组的内容。
- 量词默认贪婪,尽量多匹配
- 量词后加 ? 变懒惰,尽量少匹配
- 贪婪与否会改变捕获组内容
贪婪与懒惰的匹配推进方式
贪婪量词的工作顺序是先扩张后收缩:以 .* 为例,引擎先把它推到字符串末尾或字符类允许的最远位置,再检查后面的模式能否匹配;不能匹配就每次回退一个字符重试,直到整体匹配成功或彻底失败。所以对 "<a><b>" 执行 /<.*>/,. 会先吞到结尾,再回退到最后一个 >,结果是整串 <a><b>。
懒惰量词顺序相反,是先收缩后扩张:.*? 先匹配零个字符,立刻检查后续模式;失败才多消费一个字符再检查。用 /<.*?>/ 匹配 "<a><b>",.*? 从空开始逐步扩展,一旦遇到第一个 > 整体就成功,结果只有 <a>。两种模式找到的都是最早起点,区别只在终点停在哪里。
从日志行中提取首个尖括号标签
场景:解析构建日志,行格式固定为 "[info] <step name=\"compile\"> done <step name=\"test\"> ok",要求只提取第一个 <...> 标签内的内容。若写成 /<(.*)>/,贪婪的 .* 会一直吃到行尾再回退到最后一个 >,捕获组 1 得到 step name="compile"> done <step name="test",两个标签被连成一段,后续按 name= 解析直接出错。
改为 /<(.*?)>/ 后,.*? 每多消费一个字符就尝试匹配 >,遇到第一个 > 即停止,捕获组 1 精确得到 step name="compile"。选懒惰模式的理由是目标边界明确——第一个闭合符号就是终点;若日志保证每行只有一个标签,贪婪写法也能工作,但懒惰写法对输入变化的容错更强。
贪婪与懒惰各自失效的条件
懒惰模式容易失效在「终点不唯一」时:/a.*?b/ 匹配 "axbyb" 只到第一个 b,若你其实想要最后一个 b 之前的内容,懒惰反而取少了。贪婪模式失效在「内容中允许出现边界字符」时:/"(.*)"/ 匹配含多个引号的句子会跨越多个引号对,因为 . 本身能匹配 ",此时加 ? 也只是停在最近的引号,仍可能不是想要的配对。
更稳妥的做法是缩小字符类而不是只切换贪婪:提取引号内容用 /"([^"]*)"/,提取标签用 /<([^>]*)>/,让内容位根本无法跨过边界字符,贪婪与懒惰结果就一致了。代价是要预先知道边界字符集合;如果内容允许转义的边界字符(如 \"),还需要额外的交替分支处理,模式复杂度明显上升。
容易答错的地方
- 懒惰量词匹配最短可能字符串
.*?不是全局找最短,而是从当前起点出发、满足整体匹配的最小重复。/a.*?c/匹配"abcac"得到abc,起点仍是最早的a,不会跳过它去找更短的ac。- `?` 单独就是懒惰修饰符
?单独出现是「0 或 1 次」量词,只有紧跟在*、+、{n,m}等量词后才表示懒惰。另外{n}?语法合法但无意义,因为{n}次数固定,贪婪与否行为完全相同。
面试官还会怎么问?
贪婪和懒惰会影响匹配起点吗?
不会。两者都从字符串中最早能成功的起点开始匹配,区别只在量词控制的终点位置:贪婪尽量靠右,懒惰尽量靠左。match 返回的 index 由起点决定,与贪婪模式无关。
捕获组内外的贪婪性会互相影响吗?
会。每个量词独立决定自己的扩张或收缩顺序,前面的贪婪组会先吃掉尽可能多的字符,导致后面的组只分到剩余部分。例如 /(.+)(\d+)/ 中 .+ 会吞到只剩最后一个数字给 \d+。
`{n}` 加 `?` 有效果吗?
没有实际效果。{n} 要求精确匹配 n 次,不存在多匹配或少匹配的空间,/a{2}?/ 与 /a{2}/ 行为一致。懒惰修饰只对 *、+、{n,}、{n,m} 这类有弹性的量词有意义。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。