讲解

圆括号 ( ) 在正则里有三个作用:把多个元素捆成一个整体(配合量词和分支)、把匹配到的片段「捕获」下来供后续使用、以及在替换时引用。上一章已经见过第一个作用——/(ab)+/ 让 ab 整体重复。这一章的重点是第二个:捕获。

用 match(不加 g 标志)匹配时,返回数组的第 0 项是整个匹配,第 1 项起依次是各捕获组的内容:/(\d{4})-(\d{2})-(\d{2})/ 能把日期拆成年、月、日三段。组的编号按左括号出现的顺序数,从 1 开始。在 replace 的替换串里用 $1、$2 引用捕获组,就能做「重排格式」这类操作。

有时只想分组不想捕获——比如 (?:ID|编号) 这种「几种写法任选」的前缀,捕获它没有意义还浪费编号。(?:...) 是非捕获组:起分组作用,不占编号。ES2018 起 JavaScript 还支持命名分组 (?...),匹配结果通过 match 返回值的 groups 对象按名字取,长正则里比数括号清晰得多。

示例

// 捕获:把日期拆成年、月、日
const m = '今天是 2026-08-09'.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log('整体:', m[0], '年:', m[1], '月:', m[2], '日:', m[3]);
console.assert(m[0] === '2026-08-09');
console.assert(m[1] === '2026' && m[2] === '08' && m[3] === '09');

// replace 里用 $1 $2 $3 引用捕获组,重排日期格式
const swapped = '2026-08-09'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1');
console.assert(swapped === '09/08/2026');

非捕获组与命名分组:

// (?:...) 只分组不捕获:编号里只有 (\d+) 这一组
const m1 = '编号:123'.match(/(?:ID|编号):(\d+)/);
console.assert(m1.length === 2); // m1[0] 整体 + m1[1] 数字
console.assert(m1[1] === '123');
const m2 = 'ID:456'.match(/(?:ID|编号):(\d+)/);
console.assert(m2[1] === '456');

// 命名分组 (?<name>...):按名字取,可读性更好
const m3 = '2026-08-09'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.assert(m3.groups.year === '2026');
console.assert(m3.groups.month === '08');
console.assert(m3.groups.day === '09');

// 命名分组在 replace 里用 $<name> 引用
const us = '2026-08-09'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<m>/$<d>/$<y>');
console.assert(us === '08/09/2026');

常见坑

  • 数错组编号:编号按左括号出现顺序从 1 数,嵌套分组时尤其容易数错。组一多就该换命名分组。
  • 只想分组却顺手捕获:(ID|编号) 会白占一个编号,让后面所有组的编号错位。不需要取内容就写 (?:...)。
  • 未匹配的可选组是 undefined:/(a)?b/.exec('b') 的第 1 组是 undefined 而不是空串,取用前要有心理准备。
  • 命名分组重名:同一个正则里两个 (?...) 用同一个名字会直接抛语法错误。

小结

( ) 兼具分组与捕获,捕获组按下标 1 起取、在替换里用 $1 引用;(?:...) 只分组,(?...) 按名取用。下一节展开分组里的分支符号 |。