第 5 课:分组与选择
本课目标:
- 用捕获组
(…)包住模式的一部分,并读出它捕获的内容。- 在匹配数组里找到被捕获的片段(索引 0 是整个匹配,捕获组排在后面)。
- 用
|提供备选,并用分组控制|的作用范围。
日期匹配上了,现在把年份取出来
你现在能写 /^\d{4}-\d{2}-\d{2}$/ 来校验一个日期。但校验不等于提取:知道一个字符串是日期,并不会把年份递到你手上。还有,一个模式想同时接受 “gray” 和 “grey”,或者 “cat” 和 “dog”,该怎么办?这两个需求指向同一对元字符。圆括号身兼两职:它把模式的一部分捆在一起,并且记住那部分匹配到的内容;而 | 提供选择。要小心的是,| 的作用范围比多数人预期的更远,而圆括号正是把它管住的办法。
讲解
捕获组(capturing group)是用圆括号包起来的一段模式。它同时做两件事。第一,它把 token 捆成一束,让量词可以作用于整个组:在 /(ab)+/ 里,+ 重复的是 “ab”,匹配 “ab”、“abab”、“ababab”。第二,它记住自己匹配到的内容,供你事后取出:/(foo)/ 匹配并记住 “foo”1。
记住的文本去了哪里?进了 match 返回的数组。不带全局标志时,match 把第一个匹配作为数组返回:索引 0 是整个匹配,每个捕获组的文本按顺序排在后面:索引 1 是第一个组,索引 2 是第二个,依此类推2。捕获组的匹配结果与左圆括号的出现顺序一致1。
这就是提取的做法:把你想要的部分用圆括号包起来,然后在索引 1、2 等位置读出它们。
有时你想要分组而不想要捕获,比如只是为了让量词作用于一束你并不打算提取的 token。非捕获组(non-capturing group)(?:…) 干的就是这个:它把 token 捆在一起,但不会往结果数组里添加条目1。当圆括号只是为了托住一个量词或一段备选时,就用它。
接下来是选择运算符。析取(disjunction)| 分隔多个备选项;引擎先试左边的备选,失败了再试右边的3。所以 /cat|dog/ 匹配 “cat” 或 “dog”。微妙之处在于优先级,而它会造成真实的 bug:| 在正则里的优先级最低3。这意味着它在尽可能宽的位置把模式劈开。/^cat|dog$/ 不是“整个字符串是 cat 或 dog”;它解析成 (^cat) 或 (dog$):以 “cat” 开头,或以 “dog” 结尾。
要让 | 只作用于模式的一小段,就把那一段包进组里3。/^(cat|dog)$/ 把备选放进圆括号,锚点便包住了这个选择:整个字符串必须恰好是 “cat” 或恰好是 “dog”。gr(a|e)y 是同一个思路,它匹配 “gray” 或 “grey”,同时把共享的 “gr” 和 “y” 留在选择之外。
完整示例(跟着做)
你想把时间字符串里的小时和分钟取出来,而不只是校验它。把每个片段包进组里,然后读回来:
一步步看:模式是 (\d{2}):(\d{2})。第一个 (\d{2}) 把两位数字捕获进组 1,字面量 : 匹配冒号(它在任何圆括号之外,所以不被捕获),第二个 (\d{2}) 把两位数字捕获进组 2。match 之后,索引 0 是完整的 “09:30”,索引 1 是 “09”,索引 2 是 “30”。圆括号把一个校验器变成了一个提取器。
你来试试(填空示例)
你想要一个模式,匹配恰好是 “cat” 或 “dog” 的整个字符串:前后都不能有别的东西。填上分组,让锚点包住这个选择:
答案:空格处填 (cat|dog),得到 /^(cat|dog)$/。圆括号把 | 限制在 “cat” 与 “dog” 之间,锚点 ^ 和 $ 于是作用于整个组,整个字符串必须恰好是这两个词之一。没有圆括号的 /^cat|dog$/ 会因为 | 优先级太低而放 “category” 进门,因为它以 “cat” 开头。
小结 + 下节课预告
圆括号构成捕获组:它既把 token 捆起来供量词使用,也记住匹配到的文本,这些文本出现在匹配数组的索引 1 及之后(索引 0 永远是整个匹配)。想分组但不想捕获时用 (?:…)。| 运算符提供选择,但优先级最低,会在最宽的位置劈开模式:把它包进组里才能控制它的范围。还有一件事没弄清楚:当 + 这样的量词既能匹配短的也能匹配长的时候,它到底拿走多少?下一课我们把贪婪与懒惰讲清楚,然后你会从头到尾亲手搭建并验证一个真实的校验器。
Footnotes
-
MDN: Groups and backreferences — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Groups_and_backreferences ↩ ↩2 ↩3
-
MDN: String.prototype.match() — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match ↩
-
MDN: Disjunction (the | operator) — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction ↩ ↩2 ↩3
练习
写一个模式,匹配恰好是 “yes” 或 “no”(区分大小写)的整个字符串,然后提取出给的是哪一个。用 "yes"、"no"、"maybe" 测试。再解释为什么 /^yes|no$/(没有圆括号)是错的。
第 2 级(进阶)我的笔记
记下想法、痛点、没懂的地方。只写进这门课的附录,正课文件不动。