← 返回题目列表

JavaScript 正则的贪婪、惰性和回溯怎么理解?

中等 第 24 / 29 题 更新于 2026/07/29
JavaScriptRegExp正则

简化版

正则默认是贪婪匹配,会尽可能多地匹配;在量词后加 ? 会变成惰性匹配,尽可能少地匹配。回溯是正则引擎为了让整体模式匹配成功,尝试退回前面的匹配结果重新分配字符。

复杂正则如果写得不好,可能触发大量回溯,造成性能问题。要尽量明确边界、减少模糊重复和嵌套量词。

详细版

贪婪匹配:

"<p>a</p><p>b</p>".match(/<p>.*<\/p>/)[0];
// "<p>a</p><p>b</p>"

惰性匹配:

"<p>a</p><p>b</p>".match(/<p>.*?<\/p>/)[0];
// "<p>a</p>"

回溯例子:

/a.*b/.test("axxxb");

.* 会先吃掉尽可能多字符,发现末尾还要匹配 b,就向后退,让 b 有机会匹配成功。

性能风险常见于嵌套量词:

/^(a+)+$/.test("aaaaaaaaaaaaaaaa!");

这种模式可能产生大量组合尝试,应避免用于不可信输入。

完整版教学

一、正则匹配不是简单从左到右扫一遍

JavaScript 常见正则引擎会按模式从左到右尝试匹配,但遇到量词、分支、捕获组时,会保存一些可回退的选择点。

匹配失败时,引擎会回到选择点尝试另一种分配方式,这就是回溯。

正则回溯题的核心不是背术语,而是知道“引擎会尝试多种字符分配方案”。

二、贪婪量词默认尽可能多

*+?{m,n} 默认都是贪婪的。

const html = "<strong>A</strong><strong>B</strong>";
console.log(html.match(/<strong>.*<\/strong>/)[0]);

结果会跨过第一个 </strong>,一直吃到最后一个 </strong>

量词含义
*0 次或多次
+1 次或多次
?0 次或 1 次
{2,5}2 到 5 次

三、惰性量词尽可能少

在量词后面加 ?,会变成惰性。

const html = "<strong>A</strong><strong>B</strong>";
console.log(html.match(/<strong>.*?<\/strong>/)[0]);

它会在第一个能让整体模式成功的位置停下来。

惰性不是“不回溯”,它只是优先选择更短匹配;如果整体不成功,仍然会继续扩展尝试。

四、回溯如何发生

看这个模式:

/ab*bc/.test("abbbc");

过程可以理解为:

a matches a
b* eats bbb
next b fails at c
b* gives back one b
b matches b
c matches c

b* 先贪婪,再为了后续 bc 成功让出字符。

五、灾难性回溯的来源

灾难性回溯常来自“嵌套重复 + 模糊边界”。

const re = /^(a+)+$/;
console.log(re.test("aaaaaaaaaaaaaaaa!"));

多个 a+ 可以用很多种方式分配同一串 a。当最后的 ! 让整体失败时,引擎会尝试大量组合。

16 个 a 可能不是只尝试 16 次,而是接近指数级增长。

六、如何写更稳的正则

优化方向:

  • 用明确字符集替代 .*
  • 给重复段加清晰边界
  • 避免嵌套量词
  • 对用户输入限制长度
  • 能用解析器时不要用复杂正则解析复杂语言
const safe = /^a+$/;

如果要匹配标签内容,[^<]* 通常比 .*? 更明确。

/<p>[^<]*<\/p>/

七、常见误区与追问

  • 误区:惰性匹配一定更快。 惰性只是优先短匹配,仍可能回溯。
  • 误区:.* 总能安全匹配任意内容。 它边界模糊,容易跨越过多内容。
  • 误区:回溯一定是坏事。 回溯是正则能力的一部分,问题在于组合爆炸。
  • 误区:正则适合解析所有 HTML。 HTML 有嵌套和容错规则,复杂场景应使用解析器。
  • 追问:灾难性回溯为什么危险? 它可能让一次匹配占满主线程,形成 ReDoS 风险。
  • 追问:如何排查正则性能问题? 缩短输入、拆分模式、检查嵌套量词和模糊分支。

八、加强记忆

记三句话:

greedy: eat more first
lazy: eat less first
backtracking: try another split

面试回答时先用 <p>.*</p><p>.*?</p> 对比贪婪惰性,再用嵌套量词解释回溯风险,最后给出明确边界、限制输入、避免复杂正则解析复杂结构这些工程建议。