Skip to main content
我最早写正则只靠试,后来遇到回溯导致的卡顿,才开始看引擎是怎么匹配的。

Q1: 正则表达式的历史?为什么要有它?

1

1956年: 数学家 Kleene 的理论

数学家 Stephen Kleene 在 1956 年提出了正则集合(Regular Sets)的概念,用数学方式描述了”可以用有限状态自动机识别的语言”。这就是”正则”(Regular)这个词的来源。
2

1968年: Unix 工具 grep 的诞生

Ken Thompson 在 Unix 中实现了 grep 命令,用正则表达式搜索文本文件。从此,正则表达式从数学理论变成了实用工具。
3

1995年: JavaScript 引入正则表达式

JavaScript 1.2 引入了 RegExp 对象,让正则表达式可以在浏览器端运行。从此,正则表达式成为前端表单验证、文本处理的标配工具。
正则表达式解决的核心问题:
正则表达式是一种用模式描述字符串的语法,让复杂的文本匹配变得简洁。

🚫 没有正则表达式

判断一个字符串是否是合法的手机号,你需要手动检查:长度是否为 11 位、第一位是否是 1、第二位是否是 3-9、剩余 9 位是否都是数字。代码写出来又长又容易出错。

✅ 引入正则表达式

用一行正则就能搞定:/^1[3-9]\d{9}$/。简洁、高效、易维护。
回答时先说”为什么需要正则”(普通字符串方法的局限性),再说”正则能做什么”(模式匹配)。想听的是你对正则使用场景的理解,而不是背诵语法规则。

Q2: 正则表达式的核心语法

正则表达式由字符和元字符组成,描述一个匹配模式。
常用元字符:
默认情况下,*、+、? 是贪婪匹配——尽可能多地匹配字符。加 ? 变成非贪婪匹配——尽可能少地匹配字符。
贪婪匹配就像一个贪吃的人——看到一桌菜,他会尽可能多吃。非贪婪匹配就像一个节制的人——只吃够自己需要的量。在正则中,* 是贪吃的人,*? 是节制的人。

字符集(Character Classes)


Q3: 量词与边界

量词描述字符出现的次数,边界描述匹配的位置。
^ 和 $ 是字符串边界,不是单词边界。很多开发者混淆了这两个概念。

Q4: 分组与捕获

分组用 () 表示,可以捕获匹配的内容。
捕获组按照左括号 ( 的出现顺序编号,从 1 开始。

分组的实际应用


Q5: 断言(Lookahead & Lookbehind)

断言是一种零宽度匹配——它匹配一个位置,但不消耗字符。
断言常用于复杂的匹配场景:

Q6: 常用正则表达式实战

邮箱验证

手机号验证(中国大陆)

URL 解析

HTML 标签提取

HTML 不是正则语言(Regular Language),用正则解析 HTML 会导致各种边界问题:

Q7: 正则表达式的性能优化

正则表达式可能遇到灾难性回溯(Catastrophic Backtracking)问题,导致性能急剧下降。
以下正则模式容易导致灾难性回溯:

正则表达式的优化技巧


Q8: 正则表达式在 JavaScript 中的使用

exec 和 match 在全局匹配时行为不同:

底层源码:正则引擎是如何工作的?

正则引擎有两种实现方式:NFA(非确定性有限自动机) 和 DFA(确定性有限自动机)。JavaScript 使用 NFA(详见 V8 源码:src/regexp/regexp-parser.cc):
NFA 和 DFA 的区别: 因为 NFA 支持更多功能(反向引用、断言等),而 DFA 不支持。JavaScript 的正则表达式需要支持这些高级功能,所以选择了 NFA。

十、及格回答 vs 满分回答

问题:“说说正则表达式的匹配原理,什么是灾难性回溯?”

及格回答(60-70分):
“正则表达式用模式匹配字符串。灾难性回溯是当正则表达式有嵌套量词时,引擎需要尝试大量可能性,导致性能急剧下降。比如 /(a+)+$/ 匹配 ‘aaaaaaaaac’ 会很慢。”
问题出在:知道现象但不理解原理。 满分回答(90-100分):
正则表达式引擎有两种实现:NFA(非确定性有限自动机) 和 DFA(确定性有限自动机)。JavaScript 使用 NFA,因为它支持反向引用、断言等高级功能。NFA 的工作方式是:从起始状态开始,尝试匹配每个字符。如果匹配失败,回溯到上一个选择点,尝试另一种可能性。[1]
灾难性回溯发生在嵌套量词的场景。以 /(a+)+$/ 为例,当输入是 aaaaaaaaac 时,引擎需要尝试 a+ 匹配 1 个 a、2 个 a、…、9 个 a 的所有组合。因为有两层嵌套((a+)+),每种组合内部还需要进一步拆分,导致时间复杂度达到 O(2^n)。[2]
避免灾难性回溯的方法:1)避免嵌套量词;2)使用非捕获分组 (?:) 减少回溯开销;3)锚定开头 ^ 减少匹配尝试次数;4)使用更精确的字符集,避免过度匹配。[3]
实际项目中,正则表达式常用于表单验证、文本提取、URL 解析等场景。对于复杂的 HTML 解析,应该用 DOMParser 而不是正则——因为 HTML 不是正则语言,用正则解析嵌套标签会导致各种边界问题。

常见追问

答题要点: 默认贪婪(尽可能多),加 ? 变非贪婪(尽可能少)。高分回答: “默认情况下,*、+、? 是贪婪匹配——尽可能多地匹配字符。加 ? 变成非贪婪匹配——尽可能少地匹配字符。
贪婪匹配的原理:引擎先尽可能多地匹配,如果后续匹配失败,再回退一个字符重新尝试。非贪婪匹配相反:先尽可能少地匹配,如果后续匹配失败,再多匹配一个字符。实际应用:提取 HTML 标签内容时,必须用非贪婪匹配,否则会匹配到不正确的范围。”
答题要点: 捕获组用 (),非捕获组用 (?:),命名捕获组用 (?<name>)。高分回答: “分组用 () 表示,分为三种:
捕获组的用途:
  1. 提取数据:从字符串中提取特定部分
  2. 替换:在替换中引用捕获组($1、$2)
  3. 反向引用:在正则中引用之前的匹配(\1、\2)”
答题要点: 断言是零宽度匹配,只匹配位置不消耗字符。高分回答: “断言是一种零宽度匹配——它匹配一个位置,但不消耗字符。有四种断言:
断言的实际应用:
答题要点: exec 返回单个匹配(需循环),match 在全局模式下返回所有匹配。高分回答: “exec 和 match 的区别:
区别:
  1. 返回值:exec 返回一个匹配对象(包含 index、groups 等),match 在全局模式下返回字符串数组
  2. 捕获组:exec 返回捕获组,match 在全局模式下不返回捕获组
  3. 循环:exec 需要循环调用,match 一次返回所有匹配
答题要点: 避免嵌套量词、锚定开头、使用非捕获分组。高分回答: “优化正则表达式的方法:
  1. 锚定开头:减少匹配尝试次数
  1. 避免嵌套量词:防止灾难性回溯
  1. 使用非捕获分组:减少回溯开销
  1. 使用更精确的字符集:避免过度匹配
  1. 编译正则:复用已编译的正则
答题要点: HTML 不是正则语言,嵌套标签无法正确处理。高分回答: “HTML 不是正则语言(Regular Language),用正则解析 HTML 会导致各种边界问题:
正确做法是用 DOMParser:
为什么正则不能解析 HTML?
  1. 嵌套结构:HTML 有嵌套标签,正则无法正确匹配嵌套结构
  2. 属性复杂:HTML 属性可以包含引号、转义字符等
  3. 错误容忍:浏览器对 HTML 的解析是容错的,正则无法处理错误的 HTML”
答题要点: 嵌套量词导致指数级的回溯尝试。高分回答: “灾难性回溯发生在嵌套量词的场景。以 /(a+)+$/ 为例:
原理:
  1. 外层 + 尝试把字符串分成 1 组、2 组、…、n 组
  2. 内层 + 对每种分组方式,尝试不同的分割点
  3. 如果最后发现不匹配(因为有 c),需要回溯尝试所有可能性
  4. 总共有 2^n 种可能性(n 是 a 的个数)
避免方法:
实际项目中的例子:
答题要点: 用正则匹配模板变量,用 replace 替换。高分回答: “用正则实现一个简易的模板引擎:
进阶:支持嵌套属性
更复杂的需求可以用 new Function 实现:

延伸阅读与引用参考