讲解

竖线 | 表示「或」:/cat|dog/ 匹配 cat 或 dog。它是正则里优先级最低的运算符——两边的「阵营」会一直延伸到最近的锚点、分组边界或模式两端。这意味着 /^cat|dog$/ 并不表示「整串是 cat 或 dog」,而是「(^cat) 或 (dog$)」:cat 只被开头的 ^ 约束,dog 只被结尾的 $ 约束。想正确限定分支范围,必须把分支放进分组:/^(cat|dog)$/。

分支的匹配顺序是从左到右,先成功的分支获胜,引擎不会「选最长的」。这在分支有包含关系时会咬人:/java|javascript/ 遇到 javascript 永远只匹配到前四个字母 java,因为第一个分支已经成功了。处理有前缀关系的候选时,把长的写前面:/javascript|java/。

什么时候用 |、什么时候用字符类?判断标准是分支的单位:单个字符之间的选择用字符类([abc] 比 a|b|c 简洁高效),多字符片段之间的选择才用 |。另外每个分支都是独立的完整模式,可以带量词和分组:/(https?|ftp):/// 这种组合很常见。

示例

// | 优先级最低:不加分组时,锚点只约束最近的一侧
console.assert(/^cat|dog$/.test('cat')); // ^cat 这一支命中
console.assert(/^cat|dog$/.test('my dog')); // dog$ 这一支命中——注意这通常不是你想要的!
console.assert(/^cat|dog$/.test('catdog') === true); // 居然也过:^cat 命中

// 分组限定分支范围才是「整串是 cat 或 dog」
console.assert(/^(cat|dog)$/.test('cat'));
console.assert(/^(cat|dog)$/.test('dog'));
console.assert(/^(cat|dog)$/.test('catdog') === false);
console.assert(/^(cat|dog)$/.test('my dog') === false);

分支顺序:从左到右,先匹配先赢。

const s = 'javascript';
console.assert(s.match(/java|javascript/)[0] === 'java'); // 短分支先赢,后面的没机会
console.assert(s.match(/javascript|java/)[0] === 'javascript'); // 长的放前面才对

实战:匹配常见图片扩展名。

const img = /\.(png|jpe?g|gif|webp)$/i;
console.assert(img.test('logo.PNG')); // i 标志忽略大小写
console.assert(img.test('photo.jpeg')); // jpe?g 同时覆盖 jpg 和 jpeg
console.assert(img.test('photo.jpg'));
console.assert(img.test('doc.pdf') === false);
console.assert(img.test('pngfile') === false); // 前面必须有点号

常见坑

  • 不给分支加分组:/^cat|dog$/ 的锚点只约束半边,校验形同虚设。凡是「几个完整候选选一」的场景,写成 ^(a|b|c)$。
  • 分支顺序放反:有前缀关系的分支(java 与 javascript、https 与 http),长的必须在前,否则短分支永远先赢。
  • 用 | 做单字符选择:a|e|i|o|u 能跑但啰嗦,[aeiou] 一个字符类搞定,引擎处理也更快。
  • 分支重叠带来无谓回溯:(foo|foobar|foobaz) 三个分支共享前缀,匹配失败时每个分支都要试一遍。能合并前缀就合并:/foo(bar|baz)?/。

小结

| 表示或,优先级最低,分组限定范围;分支从左到右先匹配先赢,前缀相关的长分支放前面。下一节讲量词 matching 策略:贪婪与懒惰。