先记住这个答案
数字捕获组 (...) 按左括号出现顺序编号,结果通过 match[1]、match[2] 访问,反向引用写 \1;命名捕获组 (?<name>...) 额外把结果挂到 match.groups.name,反向引用写 \k<name>。命名组仍然占用数字序号,所以两种方式可以混用。命名组在组多、嵌套深时更可读、更抗顺序变化;数字组更简短,适合组少的场景。replace 中分别对应 $1 与 $<name>。
- 命名组结果在 match.groups.name,数字组在 match[1]
- 命名组也占数字序号,两种方式可混用
- 反向引用:命名用 \k<name>,数字用 \1
两种捕获组的编号与结果结构机制
数字捕获组 (...) 按左括号在正则中出现的顺序从 1 开始编号,嵌套时外层先编号。匹配成功后,exec 或不带 g 的 match 返回的数组里,match[0] 是整体匹配,match[1]、match[2] 依次是各组内容。命名捕获组 (?<name>...) 在此基础上额外把结果挂到 match.groups 对象上,键就是组名,且命名组仍占一个数字序号。
反向引用方面,数字组在正则内部用 \1、\2 引用之前捕获的相同子串,命名组用 \k<name>。在 replace 的替换字符串中,数字组写 $1,命名组写 $<name>。未参与匹配的可选分组,其值是 undefined,命名组同样如此,使用前要判空。
const re = /(?<year>\d{4})-(\d{2})-(?<day>\d{2})/;
const m = "date: 2024-06-15".match(re);
console.log(m[1]);
console.log(m[2]);
console.log(m.groups.year);
console.log(m.groups.day);
const dup = /(?<w>\w+)\s+\k<w>/.exec("go go stop");
console.log(dup[0]);
const out = "2024-06-15".replace(re, "$<day>/$2/$<year>");
console.log(out);查看输出与解释
2024
06
2024
15
go go
15/06/2024命名组 year、day 同时可通过数字下标 m[1]、m[3] 访问,说明命名组占序号;\k<w> 与 $<name> 分别是正则内和替换串中的命名引用。
解析日志行时命名组的工程价值
假设要解析形如 [2024-06-15 14:30:00] ERROR user=alice msg="login failed" 的日志行,提取时间、级别、用户和消息四个字段。约束是字段顺序可能随版本调整,且正则由多人维护。用数字组写成 /(\S+ \S+)\] (\w+) user=(\w+) msg="([^"]*)"/ 后,取值得记 match[3] 是用户,插入新组就会错位。
改用命名组 /(?<time>\S+ \S+)\] (?<level>\w+) user=(?<user>\w+) msg="(?<msg>[^"]*)"/,消费方写 match.groups.user,语义自解释。后续在 time 前新增一个捕获组时,数字下标全部后移,而 groups.user 不受影响,只需保证组名不重名。结果是字段抽取代码与组顺序解耦,回归出错概率显著下降。
命名组容易失效的条件与替代写法
第一,带 g 标志时 String.prototype.match 只返回整体匹配数组,连数字组都拿不到,更不会有 groups,必须改用 matchAll 或循环 exec。第二,命名组语法 (?<name>...) 需要 ES2018 支持,极老环境直接抛语法错误。第三,同一正则中组名不能重复(除非在不同分支中,较新引擎才允许),重复名会报语法错误。
对应处理:需要全量匹配时用 for (const m of str.matchAll(re)) 读 m.groups;兼容老环境时退回数字组并在代码里用常量注释下标含义,代价是可读性下降;引用不存在的组名时 \k<name> 在 /u 模式下会抛错,非 /u 下被当作字面字符,这种静默差异要用测试覆盖。组很少且稳定时直接用数字组,避免过度设计。
容易答错的地方
- 认为命名组不再占用数字序号
- 命名组依然按左括号顺序编号,
(?<y>\d+)-(\d+)中第二个组是match[2]而非match[1]。混用时数错序号会取到错误字段,应以左括号顺序统一编号。 - 用带 g 标志的 match() 读取 groups
str.match(/(?<a>\d)/g)返回的是整体匹配数组,没有index也没有groups。要拿分组信息需用matchAll或循环exec,否则读到 undefined。
面试官还会怎么问?
命名捕获组的反向引用怎么写?
正则内部用 \k<name>,如 /(?<q>['"]).*?\k<q>/ 匹配成对的同种引号;在 replace 替换串中用 $<name>。引用不存在的名字在 /u 模式下抛语法错误,非 /u 下退化为字面量。
数字组和命名组可以混用吗?
可以。所有组统一按左括号顺序编号,命名组额外出现在 groups 上。例如 /(?<a>\d)(\d)/ 中命名组同时是 match[1] 和 match.groups.a。但混用会降低可读性,组多时建议统一用命名组。
不需要捕获内容时该用什么?
用非捕获组 (?:...),它只分组不记录,不占序号、不进结果数组,也没有性能上的捕获开销。只为加量词或分支时用 (?:...),需要提取或反向引用时才用捕获组。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。