选择分支

选择分支(alternation)是正则里的"或"运算——用竖线分隔几个候选,匹配其中任意一个。看似简单,但它优先级最低这个特性,让新手经常踩"作用范围太大"的坑。这一章把这个看似简单的符号讲透。

基本用法

竖线把模式分成几个候选,引擎按从左到右的顺序尝试每个分支,第一个匹配成功的就采用。它的语法极其简单,但作用范围需要特别注意。

// 选择分支(alternation):用 | 表示"或"

cat|dog        // 匹配 cat 或 dog
cat|dog|bird   // 匹配 cat 或 dog 或 bird(三选一)
red|blue|green // 颜色枚举

// 注意优先级:| 的优先级是正则里最低的
// 下面两条完全等价(都是整个左右两边):
abc|def       // 匹配 "abc" 或 "def"
(abc)|(def)   // 同上

// 实例
/cat|dog/.test("I have a cat");   // true
/cat|dog/.test("I have a dog");   // true
/cat|dog/.test("I have a bird");  // false

// 多选项的"前缀共享"用分组更简洁
/(cat|dog|bird)s?/.test("cats");  // true (复数也匹配)

关键认知:竖线的优先级是整个正则里最低的。所以 abc|def 表示"匹配 abc 或匹配 def",而不是"匹配 ab 加 c 或 d 加 ef"。要改变这个默认范围,必须用圆括号。

用圆括号限定范围

这是新手最容易出错的地方。没有圆括号时,竖线会一路吃到模式两端——你以为限定在某个位置,实际却覆盖了整个模式。下面这个对比必须看懂:

// 用圆括号限定 | 的范围(高频技巧)

// 错误写法:| 的范围太大,匹配了 "abc" 或 "xdef"
/^abc|xdef$/.test("abc");    // true  (匹配 abc)
/^abc|xdef$/.test("xdef");   // true  (匹配 xdef)
/^abc|xdef$/.test("abcx");   // true  (! abc 开头就够,加了 x 也匹配)

// 正确写法:用括号把选择限定在中间
/^ab(c|x)def$/.test("abcdef");   // true
/^ab(c|x)def$/.test("abxdef");   // true
/^ab(c|x)def$/.test("abcxdef");  // false (不再误匹配)

// 经典场景:协议匹配 http 或 https
/^https?:\/\//.test("http://a.com");    // true (? 是更简洁的写法)
/^(https?|ftp):\/\//.test("ftp://a.com"); // true (用 | 列举多种)

// 多个长前缀用 | 配合分组
/^(Mr|Mrs|Ms|Dr)\.\s+\w+/.test("Dr. Smith");   // true
/^(!=|==|<=|>=)$/.test("==");                  // true (运算符枚举)

实用原则:只要选择分支出现在模式中间(而非整个模式的左半边或右半边),就必须用圆括号限定。是否捕获看你需不需要——不需要就加问号冒号变成非捕获组。

顺序敏感性

选择是顺序敏感的——左边的分支先尝试,匹配成功后右边就不再尝试。这意味着如果短选项在左、长选项在右,长选项可能永远没机会被匹配。这是新手第二个高频坑。

// 选择是"顺序敏感"的——左边的分支先尝试
// 这意味着长选项要写在短选项前面

// 反例:短的在前,长的永远匹配不到
/^a|ab$/.exec("ab");
// ["a"]  (匹配了左边的 a,右边的 ab 没机会)

// 正解:长的在前
/^ab|a$/.exec("ab");   // ["ab"]

// 经典案例:匹配 "January" 或 "Jan"
/^January|Jan$/.exec("January");   // ["January"] 但其实匹配到 "Jan" 部分!
// 问题:| 的范围是整个 (January) | (Jan$),左边不含 $
// 改进:用括号限定
/^(January|Jan)$/.exec("January"); // ["January"]

原则:长的分支写在前面,短的写在后面。或者更稳妥的做法——用圆括号 + 锚点明确边界,让分支之间互不干扰。这一节理解后,你会避免大量"明明应该匹配却不匹配"的诡异 bug。

选择 vs 字符类

新手常困惑:什么时候用字符类 [abc],什么时候用选择分支 a|b|c?其实有清晰的判断标准——看每个候选是单字符还是多字符

// 选择 vs 字符类:什么时候用哪个

// 1. 单字符的"或" → 用字符类(更简洁)
[a-c]    // a 或 b 或 c(等价于 a|b|c,但更短)
[aeiou]  // 元音字母

// 2. 多字符的"或" → 只能用选择分支
cat|dog         // 字符类无法表达
\d{3}|[A-Z]{2}  // 三位数字 或 两位大写字母

// 3. 性能差异:字符类通常比选择快
//    字符类是"一次比较",选择是"先试左边,失败再试右边"

// 实例对比
// 匹配 "color" 或 "colour"
/colou?r/.test("color");    // true  (? 优于 |)
/color|colour/.test("colour"); // true (但啰嗦)

// 匹配任意一位数字或字母
/[0-9a-zA-Z]/    // 字符类比 /\d|[a-zA-Z]/ 更快更清晰

// 但匹配 "fi" "km" "mi"(英里单位缩写)只能用 |
/fi|km|mi/

实战组合

选择分支在实际项目里几乎都是配合分组、锚点、单词边界一起用。看几个真实场景:

// 实战:枚举常见值

// 1. 一周七天
/^\b(Mon|Tues?|Wed(nes)?|Thurs?|Fri|Sat|Sun)day\b$/i

// 2. 常见 HTTP 方法
/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$/

// 3. 中国省份(简化)
/^北京|上海|广东|浙江$/

// 4. 文件扩展名(用分组限定)
/^\w+\.(jpg|jpeg|png|gif|webp)$/i.test("photo.jpg");   // true

// 5. 关键字过滤(避免与子串误匹配)
/\b(spam|ads|scam)\b/i.test("this is spam");   // true

注意枚举类场景(星期、HTTP 方法、文件扩展名)必须配合单词边界或锚点,否则会匹配到子串——比如不加边界,Mon 会连 Monday 里的 Mon 也匹配,但你也匹配不到 Monster 里的 Mon(如果那不是你想要的)。

小结

下一章讲正则的"高级技巧"——零宽断言(环视),让你"看一下但不消费字符"。

← 上一篇 分组与捕获

下一篇 零宽断言(环视)

✈️💬