量词下的分组:只剩最后一次
"123".replace(/(\d)+/, "$1") 得到的是 '3',既不是 '123' 也不是 '1'。命名捕获与反向引用
说过命名捕获只是给组起别名,这页要说的是另一件事:一个捕获组只有一个槽,量词每重复一次就把上一次的值覆盖掉。整串确实匹配了,但只能拿到最后那一次。
1 · 量词在组外与在组内
一字之差,结果完全不同。(\d)+ 是「重复这个组」——组每次只吃一个数字,被重复三次,槽里留下最后一次;(\d+) 是「组里装了个 \d+」——组只匹配一次,一口气吃下整串。
2 · 单步看槽被覆盖
把量词的每一次重复拆成一步,就能看清那个槽的一生:每迭代一次写入一个新值,上一次随即丢失,最后停下时槽里剩的就是 $1。
3 · 别的语言的取舍
「只留最后一次」是 JS、Python、Java 与 PCRE 的共同选择,但不是唯一可能:那些被覆盖掉的值,有的语言留着。
| 语言或引擎 | 量词下的分组拿到什么 |
|---|---|
| JS、Python、Java、PCRE | 只有最后一次 |
| .NET | 默认值是最后一次,但 Group.Captures 留着每一次 |
| Perl | 默认最后一次;可用 (?{ … }) 内嵌代码观察匹配过程 |
| Raku | 直接把多次匹配收进一个 list |
所以「只捕获最后一个」不是它没匹配,而是语言只保留了一个槽。引擎内部一次次匹配、一次次覆盖,Perl 可以打印过程,Raku 干脆把每次都收进列表,.NET 用 Captures 集合留底——同一个现象,不同语言给了不同的取舍。
4 · 放进后行断言
既然留下的是最后一次,把同样的结构塞进后行断言会怎样?"123".match(/(?<=(.){3})/) 的 $1 是 "1",不是 "3"。因为后行断言是从右往左匹配的:引擎站在位置 3,往左依次吃
3、2、1,最后一次迭代吃到的正是最左边那个字符。规则没变——留下的仍是最后一次——只是「最后一次」在方向反转后落到了串首。
注 · 这个现象与 零宽匹配 是一对:那页讲匹配可以不消耗字符,这页讲一次匹配里重复的捕获只留得下一个值。两者合起来解释了大多数「结果与直觉不符」的捕获问题。
5 · 参考文献
- Ecma International. ECMA-262: RepeatMatcher. 量词重复时捕获组被重置与写入的规范步骤。tc39.es
- Microsoft. Group.Captures. .NET 保留每次捕获的接口,本页对照表里那一行的出处。learn.microsoft.com
- MDN. Lookbehind assertion. 后行断言从右往左匹配的语义,第 4 节那道题的依据。developer.mozilla.org