6.11 正则表达式:模式匹配、捕获组、先行/后行断言、matchAll、Unicode 属性转义

模式匹配、捕获组、先行/后行断言、matchAll、Unicode 属性转义

正则RegExp模式匹配断言

原理

正则表达式(Regular Expression)是一种描述字符串模式的声明式语言。JavaScript 的正则引擎基于 NFA(非确定有限自动机)回溯实现,支持大部分 PCRE 特性,并在 ES2018 后大幅增强了 Unicode 支持。

RegExp 的创建与标志

正则可通过字面量 /pattern/flagsnew RegExp('pattern', 'flags') 创建。后者支持动态构建模式,但需要对反斜杠进行双重转义。

标志(Flags):

  • g(global):全局匹配,找到所有匹配而非第一个。
  • i(ignoreCase):忽略大小写。
  • m(multiline):^$ 匹配每行的开头和结尾。
  • s(dotAll,ES2018):. 匹配包括换行符在内的所有字符。
  • u(unicode,ES2015):启用 Unicode 模式,正确处理码点大于 ￿ 的字符。
  • y(sticky,ES2015):粘性匹配,从 lastIndex 开始严格匹配。
  • d(hasIndices,ES2022):匹配结果包含每个捕获组的起始/结束索引数组。

捕获组与反向引用

圆括号 (...) 创建捕获组,匹配结果存储在 RegExp.$1$2 或结果数组中。非捕获组 (?:...) 仅用于分组,不保存匹配内容。

命名捕获组(ES2018):(?<name>...),通过 match.groups.name 访问。

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = re.exec('2024-03-15');
console.log(result.groups.year); // '2024'

先行断言与后行断言

断言(Assertion)匹配位置而非字符,不消耗输入字符串。

  • 正向先行断言x(?=y),匹配后面跟着 yx
  • 负向先行断言x(?!y),匹配后面不跟着 yx
  • 正向后行断言(ES2018):(?<=y)x,匹配前面有 yx
  • 负向后行断言(ES2018):(?<!y)x,匹配前面没有 yx
// 匹配价格数字(前面有 $ 符号)
const prices = 'Apple $1.50, Banana $0.80';
const priceRe = /(?<=\$)\d+\.\d+/g;
prices.match(priceRe); // ['1.50', '0.80']

matchAll 与迭代器

String.prototype.matchAll(ES2020)返回一个迭代器,产出所有匹配结果(包括捕获组信息),避免了 match 在全局模式下丢失捕获组的问题。

const str = 'test1 test2';
const re = /t(e)(st(\d?))/g;

// match 在 g 模式下返回 ['test1', 'test2'],丢失捕获组
// matchAll 保留所有信息
for (const match of str.matchAll(re)) {
  console.log(match[0], match[1], match[2], match[3]);
}

Unicode 属性转义(ES2018)

u 标志下,可使用 \p{Property}\P{Property} 匹配 Unicode 字符属性:

const emojiRe = /\p{Emoji}/gu;
'Hello 👋 World 🌍'.match(emojiRe); // ['👋', '🌍']

// 匹配所有中文字符
const chineseRe = /\p{Script=Han}/gu;
'中文123'.match(chineseRe); // ['中', '文']

用法

// 使用命名捕获组进行 URL 解析
const urlRe = /^(?<protocol>https?):\/\/(?<host>[^\/]+)(?<path>.*)$/;
const { groups } = urlRe.exec('https://example.com/path?query=1');
// groups: { protocol: 'https', host: 'example.com', path: '/path?query=1' }

// 使用 matchAll 提取所有代码块
const markdown = '```js\nconst x = 1;\n```\n```py\nx = 1\n```';
const codeRe = /```(?<lang>\w+)\n(?<code>[\s\S]*?)\n```/g;
const blocks = [...markdown.matchAll(codeRe)];
blocks.forEach(([_, lang, code]) => console.log(lang, code));

// 安全的用户名校验(允许字母、数字、下划线、连字符)
const usernameRe = /^[a-zA-Z0-9_-]{3,20}$/;

实践

正则性能优化

正则引擎使用回溯算法,某些模式会导致灾难性回溯(Catastrophic Backtracking):

// 危险:嵌套量词 + 模糊匹配
const badRe = /(a+)+b/;
badRe.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac'); // 极慢,指数级回溯

优化策略:

  • 使用具体字符类替代 .
  • 避免嵌套量词(如 (a+)+)。
  • 使用占有量词(某些引擎支持)或原子组(JavaScript 不支持,需模拟)。
  • 对长字符串匹配设置超时或长度限制。

正则的测试与调试

  • 使用 regex101.com、regexr.com 等在线工具可视化匹配过程。
  • 在 Node.js 中使用 console.time 测量正则执行时间。
  • 对复杂正则添加注释(new RegExp 配合字符串拼接)。

陷阱

| 陷阱 | 现象 | 解决方案 | |------|------|---------| | 全局标志与 exec 的 lastIndex | 重复调用 exec 可能从意外位置开始 | 每次使用新的 RegExp 实例;或在循环中注意 lastIndex | | match 在 g 模式下丢失捕获组 | 结果仅为字符串数组 | 使用 matchAll 或多次 exec | | 点号不匹配换行 | 多行文本匹配失败 | 使用 [\s\S] 或启用 s(dotAll)标志 | | Unicode 字符被拆分为代理对 | u 标志未启用时,😀 被视为两个字符 | 始终对含 Unicode 的正则启用 u 标志 | | 正则对象的 test 修改 lastIndex | g 标志下 test 也推进 lastIndex | 非循环场景避免对同一正则实例混用 test/match/exec |

测验

关联章节网络

当前章节
关联章节
交叉引用
后续延伸