跳到主要内容

分组与回溯引用

括号给模式带来哪两件事?​

  • 语法: (...) 把一段模式包成"捕获组";
  • 效果一: 匹配到的内容额外出现在结果数组里, 可单独取出;
  • 效果二: 量词作用于整个括号, 而不是只作用于最后一个字符;
// go+ : g 后跟一个或多个 o
"Gogogo now!".match(/(go)+/gi); // Gogogo
  • 对比: go+ 能匹配 goooo; (go)+ 匹配的是 gogo、gogogo;

如何用分组拼出域名与邮箱?​

  • 域名: 若干"词 + 点", 最后接一个词 → (\w+\.)+\w+;
  • 缺陷: \w 不含连字符, my-site.com 漏匹配 → 换成 [\w-], 得 ([\w-]+\.)+\w+;
  • 邮箱: name@domain, name 允许 - 与 . → [-.\w]+@([\w-]+\.)+[\w-]+;
  • 现实提醒: 正则只能挡住笔误, 邮箱真伪只能靠真发一封信验证;
"site.com my.site.com".match(/(\w+\.)+\w+/g); // site.com, my.site.com
"my@mail.com @ his@site.com.uk".match(/[-.\w]+@([\w-]+\.)+[\w-]+/g);

结果数组里的分组是怎么编号的?​

  • 顺序: 按左括号出现的先后, 从左到右;
  • result[0]: 整个匹配, 永远占 0 号位;
  • result[1]、result[2]、…: 第 1、2、… 个左括号捕获的内容;
const tag = "<h1>Hello, world!</h1>".match(/<(.*?)>/);
tag[0];
tag[1]; // <h1>, h1 — 整个匹配, 第一个组的内容

嵌套分组的编号规律是什么?​

  • 规律: 仍按左括号从左到右, 与嵌套层级无关;
const r = '<span class="my">'.match(/<(([a-z]+)\s*([^>]*))>/);
r[0];
r[1]; // '<span class="my">', 'span class="my"' — 整个匹配, 第 1 个左括号
r[2];
r[3]; // 'span', 'class="my"' — 第 2、3 个左括号
  • 读法: 每个左括号算一次"开单", 开单顺序就是编号顺序, 忽略右括号位置;

可选组没匹配到时结果里是什么?​

  • 规则: 组仍占位, 值为 undefined; 数组长度固定, 与实际匹配无关;
"a".match(/a(z)?(c)?/); // ['a', undefined, undefined], 长度 3
"ac".match(/a(z)?(c)?/); // ['ac', undefined, 'c'], 长度 3
  • 坑: 不能用 length 判断"匹配到几个组", 必须逐个比对 undefined;
  • 附带信息: 匹配数组还带 index (匹配位置) 与 input (源串); regexp.exec 在无 g 时返回同样结构;

命名组如何免去数括号?​

  • 写法: 左括号后紧跟 ?<name>, 即 (?<name>...);
  • 取值: 命名组同时放进匹配结果的 .groups 对象;
const re = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const { year, month, day } = "2019-04-30".match(re).groups; // 2019 04 30
  • 取全部匹配: 加 g 后 str.match 会丢弃分组, 需改用 matchAll, 见 正则方法与回溯风险; 组数多时名字比序号可读得多;

分组内容如何用在替换字符串里?​

  • 按号引用: $n, n 为组号;
  • 按名引用: $<name>;
"John Bull".replace(/(\w+) (\w+)/, "$2, $1"); // Bull, John
"2019-10-30".replace(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/, "$<day>.$<month>.$<year>"); // 30.10.2019
  • 记号区分: 模式内部引用用反斜杠 \1, 替换串里用美元 $1;
  • 替换串的其他占位符与函数式替换: 见 正则方法与回溯风险;

非捕获组 (?:...) 有什么用?​

  • 写法: 左括号后跟 ?:;
  • 作用: 只用来施加量词, 不单独占结果位置, 也不参与编号;
  • 副作用: 无法被 \N 引用, 也无法在替换串里引用;
const result = "Gogogo John!".match(/(?:go)+ (\w+)/i);
result[0]; // Gogogo John
result[1]; // John
result.length; // 2, 没有多出来的 'go'

怎样在模式内部引用刚捕获的文本?​

  • 按号: \N, N 为组号; 按名: \k<name>;
  • 动机: 配对定界符, 例如开闭引号必须是同一种;
const str = `He said: "She's the one!".`;
str.match(/['"](.*?)['"]/g); // ["She' → 引号混配, 错
str.match(/(['"])(.*?)\1/g); // "She's the one!" → 引号配对, 对
str.match(/(?<quote>['"])(.*?)\k<quote>/g); // 同上, 按名引用
  • 机制: 引擎记住 (['"]) 实际匹配到的那个字符, \1 要求后面出现完全相同的文本;
  • 限制: (?:...) 的内容不被记忆, 因此无法引用;

交替 | 的匹配范围由什么决定?​

  • 语义: | 就是"或", 优先级最低, 范围一直延伸到所在模式的边界;
  • 与方括号的区别: [ae] 只能选字符或字符类, A|B|C 可选任意子表达式;
  • 等价与陷阱:
    • gr(a|e)y 等价于 gr[ae]y, 都匹配 gray 或 grey; gra|ey 则匹配 gra 或 ey, 模式被从中间切开;
    • I love HTML|CSS 匹配 I love HTML 或 CSS; I love (HTML|CSS) 才两条都带 I love;
  • 结论: 想限定 | 的范围, 必须用括号把它括起来;

用交替写时间校验要注意什么?​

  • 目标: 只接受 00:00–23:59, 拒绝 25:99;
  • 小时: 首位 0/1 时次位任取 → [01]\d; 首位 2 时次位限 0-3 → 2[0-3]; 其他首位非法;
  • 分钟: [0-5]\d;
  • 陷阱: 直接拼成 [01]\d|2[0-3]:[0-5]\d, | 把分钟只分给了第二个分支;
  • 修正: 用括号把"小时"整体括住 → ([01]\d|2[0-3]):[0-5]\d;
"00:00 10:10 23:59 25:99 1:2".match(/([01]\d|2[0-3]):[0-5]\d/g);
// 00:00, 10:10, 23:59
  • 规律: 谁被括进括号, 谁才共享括号后面的模式;