先记住这个答案
命名捕获组就是给分组起名字,写成 (?<name>...),匹配后通过结果上的 groups 对象按名字取值,比如 m.groups.year。它和普通的数字捕获组并不冲突,命名组照样按顺序占一个下标,m[1] 仍然能取到。好处主要在维护:正则里新增或删除一个分组,后面所有数字下标都会跟着错位,按名字取值则不受影响。替换字符串里用 $<name> 引用,正则内部反向引用用 \k<name>。
- 写法
(?<year>\d{4}),读取m.groups.year - 命名组照样占数字下标,
m[1]也能取到 - 替换字符串用
$<name>,正则内部用\k<name> - 可选分支没命中时,对应的值是
undefined
用日期解析看两种写法的区别
最典型的场景是从一段文本里拆出多个字段,比如从日志里取出年月日。
groups 拿到每个字段JavaScriptconst re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const m = re.exec('发布于 2026-09-29')
// 命名组可以直接解构,数字下标也照常可用
const { year, month, day } = m.groups
console.log(m[1], year, month, day, m.index)查看输出与解释
2026 2026 09 29 4m[1] 和 m.groups.year 拿到的是同一个值,命名只是多提供了一种按名字访问的方式。
这时候看不出命名组有多大优势。设想需求变了,日期前面可能带一个可选的时区前缀,正则前面要加一个分组 (UTC\+\d )?。用数字下标的代码,m[1] 就从年份变成了时区,后面的 m[2]、m[3] 全部错位,而且不会报错,只会悄悄取错值。用 groups 的代码一行都不用改。
正则越长、分组越多,这个差别越明显。解析 URL、日志、版本号这类有好几个字段的场景,命名组基本是标配。反过来,只有一个分组的短正则,用 m[1] 也完全没问题。
在 replace 里引用命名组
replace 有两种用法:第二个参数是字符串,或者是回调函数。两种用法都能按名字引用分组。
const s = '2026-09-29'
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
// 替换字符串里用 $<name>
console.log(s.replace(re, '$<day>/$<month>/$<year>'))
// 回调里:有命名组时,最后一个参数就是 groups 对象
const out = s.replace(re, (...args) => {
const g = args[args.length - 1]
return `${g.year}年${Number(g.month)}月`
})
console.log(out)查看输出与解释
29/09/2026
2026年9月替换字符串里的语法是 $<name>,和 $1 同一套规则,引擎在替换时识别。回调函数的参数依次是完整匹配、每个捕获组、匹配位置、原字符串,正则里有命名组时,最后还会多一个 groups 对象。因为前面参数的个数取决于分组数量,所以直接取最后一个参数最稳,不用去数位置。
- 正则内部的反向引用
\k<name> - 反向引用的意思是「这里要出现和前面某个组一模一样的内容」。数字写法是
\1,命名写法是\k<name>。比如/(?<word>\w+) \k<word>/能找出go go这种重复的单词,对go to则匹配失败。检查 HTML 开闭标签是否成对、引号是否前后一致,都会用到这种写法。
容易答错的地方
- 在替换字符串里写
${name} ${name}是模板字符串的语法,放在普通单引号字符串里就是字面文本,'go'.replace(/(?<w>o)/, '${w}')得到的是g${w}。如果写在反引号模板字符串里,它会在调用replace之前就去读一个叫name的变量,和捕获组无关。正确写法是$<name>。- 以为没命中的命名组不会出现在
groups里 /(?<num>\d+)|(?<word>[a-z]+)/去匹配abc,只有word分支命中,但groups里num这个键仍然存在,值是undefined。用JSON.stringify打印时undefined会被省略,看起来像是没有这个键,其实'num' in m.groups是true。所以读可选分支的组时要先判空,别直接调字符串方法。
面试官还会怎么问?
正则里没写命名组时,groups 是什么?
是 undefined。所以写通用工具函数时,const { a } = m.groups 要改成 const { a } = m.groups ?? {},否则正则里没有命名组就会抛 TypeError。有命名组时,groups 是一个没有原型的对象,只包含组名,不会有 toString 这类继承来的属性。
同一个名字能用两次吗?
在同一个分支里重复命名会直接报 SyntaxError,正则都创建不出来。组名也要符合标识符规则,不能以数字开头,不能带连字符,比如 (?<first-name>...) 是非法的,要写成 firstName。
只想分组、不想捕获怎么写?
写成非捕获组 (?:...)。比如 (?:https?|ftp):\/\/ 里的括号只是为了让 | 的作用范围限定在协议上,不需要读取它。多余的捕获组不仅占下标、让后面的编号更难数,也会让结果对象里多出无用的值。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。