正则表达式
JavaScript 使用 ECMAScript RegExp 实现,支持贪婪/惰性量词、断言与 matchAll 等。
一、贪婪与惰性匹配
1. 贪婪匹配(默认)
量词(*、+、?、{m,n} 等)在默认情况下会匹配尽可能多的字符。
const str = "<div>content</div><span>text</span>";
const greedy = /<.*>/; // 匹配从第一个 < 到最后一个 >
console.log(greedy.exec(str)[0]);
// 输出: "<div>content</div><span>text</span>" (整个字符串)
2. 惰性匹配(非贪婪)
在量词后添加 ? 使其匹配尽可能少的字符。常用语提取 HTML 标签内的内容。
const lazy = /<.*?>/; // 匹配最短的 <...>
console.log(lazy.exec(str)[0]);
// 输出: "<div>" (只匹配第一个标签)
常用惰性量词:*?、+?、??、{m,n}?。
二、反向引用(Backreference)
反向引用允许你在正则表达式中引用前面捕获组匹配的内容。语法为 \1, \2 ...(数字对应捕获组顺序)。
1. 匹配重复单词
const str = "hello hello world";
const regex = /\b(\w+)\s+\1\b/;
console.log(regex.test(str)); // true
2. 匹配成对标签
const html = "<div>content</div><span>text</p>";
const tagRegex = /<([a-z]+)>.*?<\/\1>/i;
console.log(tagRegex.test(html)); // false,因为第二个标签不匹配
3. 在替换中使用 $1
const date = "2025-04-25";
const newDate = date.replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1");
console.log(newDate); // "25/04/2025"
三、前瞻与后顾(Lookahead & Lookbehind)
断言匹配位置而不消耗字符,分为正向(肯定)和负向(否定)。
| 类型 | 语法 | 含义 |
|---|---|---|
| 正向前瞻 | (?=...) | 后面必须跟着 ... |
| 负向前瞻 | (?!...) | 后面不能跟着 ... |
| 正向后顾 | (?<=...) | 前面必须是 ...(ES2018 支持) |
| 负向后顾 | (?<!...) | 前面不能是 ...(ES2018 支持) |
1. 正向前瞻:匹配后面是数字的字母
const str = "a1 b2 c3 d";
const regex = /[a-z](?=\d)/g;
console.log(str.match(regex)); // ['a','b','c']
2. 负向前瞻:匹配后面不是数字的字母
const regex2 = /[a-z](?!\d)/g;
console.log(str.match(regex2)); // ['d']
3. 正向后顾:匹配前面是 @ 的用户名
const text = "email: user@example.com, contact@test.org";
const regex = /(?<=@)[a-z0-9.-]+\.[a-z]+/g;
console.log(text.match(regex)); // ['example.com', 'test.org']
4. 负向前瞻:匹配协议不是 https 的 URL
const urls = "http://example.com https://secure.com ftp://files.com";
const regex = /\b(?!https:\/\/)\w+:\/\/[^\s]+/g;
console.log(urls.match(regex)); // ['http://example.com', 'ftp://files.com']
注意:此处用负向前瞻而非后顾——\b 定位在协议名开头,紧接着判断后面是否为 https://。若改用后顾 (?<!https:\/\/)\b...,断言检查的是 \b 前面的内容,而字符串开头或空格处并不是 https://,因此该断言恒为真,不会起到排除作用,实际会匹配出全部三个 URL。
四、exec 与 matchAll 的迭代使用
1. RegExp.prototype.exec
每次调用返回下一个匹配结果(带捕获组),直到 null。需要手动维护 lastIndex。
const regex = /(\d{4})-(\d{2})-(\d{2})/g;
const str = "2025-04-25, 2026-01-01";
let match;
while ((match = regex.exec(str)) !== null) {
console.log(
`Full: ${match[0]}, Year: ${match[1]}, Month: ${match[2]}, Day: ${match[3]}`
);
}
2. String.prototype.matchAll
ES2020 引入,返回可迭代对象,更简洁。必须在正则上设置 /g 标志。
const regex = /(\d{4})-(\d{2})-(\d{2})/g;
const str = "2025-04-25, 2026-01-01";
for (const match of str.matchAll(regex)) {
console.log(`Year: ${match[1]}, Month: ${match[2]}, Day: ${match[3]}`);
}
优势:无需手动处理 lastIndex,且每次迭代返回的数组结构与 exec 相同(包含捕获组)。
五、常见性能陷阱:灾难性回溯(Catastrophic Backtracking)
当正则表达式包含嵌套量词或重叠的选择分支时,引擎可能尝试指数级的路径,导致 CPU 飙升甚至挂起。
1. 危险模式示例
// 极其危险的表达式
const dangerous = /^(a+)+$/;
// 匹配 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX" 时,会逐层回溯,耗时极长
典型危险模式:
(a+)+、(a|a)+、(a*)*(a+)*b与长串不带b的a(\w+)+配合长度较大的字符串
2. 为什么会发生?
正则引擎(如 PCRE、JavaScript 的 V8)在进行分支选择时,会尝试所有可能路径。遇到 (a+)+ 匹配完 a 后,由于外层的 + 可以重复,回退时会产生大量排列组合。
3. 如何避免?
- 使用原子分组(JavaScript 本身不支持,但可通过前瞻模拟)
js
// 模拟原子分组 (?=(a+))\1 const safe = /^(?=(a+))\1+$/; - 占有量词(JS 也不支持,但可以转换为非回溯结构)
- 简化表达式:避免不必要的嵌套量词。
js
// 坏:/^(a+)+$/ // 好:/^a+$/(如果只需要匹配一个或多个 a) - 限制输入长度:在应用层面限制字符串长度。
- 使用
String.prototype.search或indexOf替代正则(对于简单的子串匹配)。 - 使用多个独立正则代替一个复杂正则。
- 工具检测:使用
safe-regex库或在线分析工具(如 regex101.com)查看回溯次数。
4. 真实案例:检测空格的 /\s*,\s*/ 是安全的,但 /(a|aa)*b/ 匹配 aaaaaaaa...(无 b)时可能灾难。
总结
| 概念 | 关键点 |
|---|---|
| 贪婪 vs 惰性 | 默认贪婪(尽可能多),加 ? 变惰性(尽可能少) |
| 反向引用 | \1 引用捕获组内容;替换中 $1 使用 |
| 前瞻/后顾 | 零宽断言:(?=...)、(?!...)、(?<=...)、(?<!...)。后顾需 ES2018+ |
| 迭代匹配 | exec 需手动循环;matchAll 返回迭代器,更现代 |
| 性能陷阱 | 避免嵌套量词和重叠选择,警惕灾难性回溯;可用原子组模拟或简化表达式 |
复杂 HTML/XML 解析优先用专用解析器,勿用正则勉强匹配嵌套结构。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| MDN:RegExp | 正则对象 |
| MDN:Regular expressions | 指南 |
| MDN:matchAll | 迭代匹配 |
| ECMA-262:RegExp | 规范(英文) |
相关文章
错误处理与调试
结构化错误类型、try/catch 与全局未捕获处理是健壮应用的基础。
浏览器相关 API
DOM 事件、存储、fetch、Worker、Observer 等由浏览器提供的 Web API(非 ECMAScript 核心)。HTML 结构见 标签。
性能优化
防抖/节流、渲染与内存、资源预加载及引擎行为是前端性能的主要抓手。相关 CSS 见 视觉效果、Head 资源提示。
模块化与工程化
ESM、CommonJS 与打包工具(Tree Shaking、代码分割)是前端工程化的基础。入门见 JavaScript 基础 · 模块。
执行模型与作用域
执行上下文与词法作用域决定变量在何处可访问、闭包如何形成,以及 var / let / const 的差异。语言概览见 JavaScript 基础。
数据结构与集合增强
Map / Set、WeakMap / WeakSet、数组高阶方法、TypedArray 与 Symbol 等扩展了内置数据结构能力。见 语言基础。