组装:把模式搭起来
选择分支与优先级
它在解决什么
| 就是「或」:cat|dog 匹配 cat 或者 dog。
看起来没什么可讲的,但它有两个性质会稳定地坑到人,而且两个都不报错:
- 它的优先级低到超出直觉——低到会把你以为是一个整体的模式劈成两半。
- 它选分支的规则是「第一个能匹配的赢」,不是「最长的赢」。
这一篇基本就是围绕这两件事。
🚨 | 的优先级最低
先看这条模式,猜一下它匹配什么:
/^cat|dog$/
多数人读成「整串是 cat 或者 dog」。它实际读作:
(^cat) | (dog$) ← 真实含义
^(cat|dog)$ ← 你以为的含义
所以 /^cat|dog$/.test('cat food') 返回 true——
'cat food' 确实以 cat 开头,左边那一支成立了。而正确写法
/^(cat|dog)$/ 对同一个输入返回 false。
这条模式作为校验用是完全失效的:它会放行任何以 cat 开头的东西,
以及任何以 dog 结尾的东西。而它不报错、不警告,对 'cat' 和 'dog'
这两个测试输入还都是对的——写测试的人很容易就此收工。
⚠️ 注意 | 的优先级比锚点还低。量词那篇说的「只管紧挨着的一个元素」
是同一类问题的另一面:| 恰好相反,它管得太宽,一直吃到模式的两端为止。
括号是控制它的唯一办法
| 没有「只作用于这几个字符」的写法,圈定范围只能靠括号:
/\b(cat|dog)s\b/g 从 'cats dogs cows' 里取出 cats 和 dogs。
去掉那对括号之后,模式读作 (\bcat) | (dogs\b)——前一支不要求后面有 s,
后一支不要求前面有边界,你以为的「公共部分」\b 和 s 被两支各自吃掉了。
👉 拿不准就加括号。 这是本篇唯一需要背的一条:多余的括号最多让模式长一点
(用 (?:…) 连编号都不会占),而漏掉的括号会安静地改变整条模式的含义。
分支和锚点那篇的词边界经常要一起用。
'cat category dog'.replace(/\b(cat|dog)\b/g, 'pet') 得到 'pet category pet',
而少了 \b 的版本会把 category 改成 petegory——批量替换是最容易出这种事故的地方,
它不报错,只是悄悄改坏几个不该动的词。
🚨 第一个能匹配的赢,不是最长的赢
'category'.match(/cat|category/) 得到的是 'cat',
不是 'category'。
引擎在同一个起点从左往右逐个试分支,第一支成功就收工,根本不会去看第二支
有没有可能匹配得更长。把两支调换顺序,结果就变成 'category'——
所以分支的顺序是有语义的,不是随便排的。
👉 判据:把长的、具体的放前面,短的、笼统的放后面。
/category|cat/ 是对的,/cat|category/ 几乎总是 bug。
📌 这条只对回溯引擎成立(JavaScript、Python、Perl、Java 都是)。 POSIX 那一系的工具规定的是「最左最长」匹配,同一条模式结果正好相反:
$ echo category | grep -oE 'cat|category'
category ← grep 给最长的
$ node -e "console.log('category'.match(/cat|category/)[0])"
cat ← JS 给最先匹配上的
(实测环境:/usr/bin/grep = BSD grep 2.6.0-FreeBSD、node v22、Python 3.14;
python3 的结果与 node 一致。最左最长是 POSIX 规定的行为,所以 GNU grep 同理。)
这类差异会在方言那一篇集中讲,这里只需要记住一件事:
「分支顺序不重要」这个念头在任何一边都是错的,只是错的方式不同。
[…] 和 (…|…) 不能互换
两者读起来都是「或」,但能表达的东西差一个量级:
| 挑的是 | 能否多字符 | 是否捕获 | |
|---|---|---|---|
[cd] |
一个字符 | 不能 | 不捕获 |
(c|d) |
一整段 | 能 | 捕获(除非写 (?:) |
/^(cat|dog)$/ 匹配 'dog',而 /^[catdog]$/ 匹配不了——
后者说的是「一个字符,且它属于 c/a/t/d/o/g 这个集合」,而 'dog' 有三个字符。
⚠️ [catdog] 这个写法本身是合法的,也不会报错。它只是几乎肯定不是你要的意思——
方括号里没有「或」的概念,也不认识「单词」,它眼里只有一堆互不相干的字符。
👉 判据:候选项都是单个字符就用 […](更快、更短、不占编号),
只要有一个候选项超过一个字符,就必须用 (…|…)。
优先级速查
从高到低:
1. \ 转义
2. ( ) [ ] 分组与字符类
3. * + ? {} 量词
4. 序列、锚点 abc ^ $ \b
5. | 分支 ← 最低
这张表只有最后一行需要记——前四行基本符合直觉,而第 5 行是本篇所有坑的来源。
怎么选
| 你要表达的 | 写法 |
|---|---|
| 这一个字符是 a、b 或 c | [abc] |
| 这一段是 cat 或 dog | (?:cat|dog),要取内容才写 (cat|dog) |
| 整串正好是 cat 或 dog | ^(cat|dog)$ —— 别写 ^cat|dog$ |
| 只替换独立的词 | \b(cat|dog)\b |
| 候选项有长有短 | 长的写前面 |
一条实用的自查:写完带 | 的模式,把它读成「(…) 或 (…)」再看一遍。
如果你读出来的两支跟你想的不一样,就是括号加漏了。
下一步
《先行与后行》——本章最后一块。它们和锚点一样是零宽的,
但能表达的条件远不止「行首行尾」:比如「后面跟着的不是汉字」这种,
正是\b 对中文无效那一节留下的问题。
本篇示例
下面每一条都由 npm run test:regex 在每次构建前实跑验证, 结果是现算的,不是抄进数据里的副本。正文里的代码块只用来演示匹配过程和写法对照,不进闸门; 凡是「这个模式配这个输入得到这个结果」的断言,只存在于这里。
🚨
|的优先级低到会把整条模式劈成两半- 调用
/^cat|dog$/.test("cat food")- 结果
true- 换成
/^(cat|dog)$/ false
|的优先级比所有东西都低,包括锚点。拿不准就加括号 —— 加括号从来不会让事情变糟。分支是「第一个能匹配的赢」,不是「最长的赢」
- 调用
"category".match(/cat|category/)- 结果
["cat"]- 换成
/category|cat/ ["category"]
🚨 正则不做最长匹配。写分支时把长的、具体的放前面,短的、笼统的放后面。
[…]和(…|…)不能互换- 调用
"dog".match(/^(cat|dog)$/)- 结果
["dog","dog"]- 换成
/^[catdog]$/ null
字符类挑的是一个字符,分支挑的是一整段。看起来都是「或」,能表达的东西差一个量级。
括号决定分支「管到哪为止」
- 调用
[..."cats dogs cows".matchAll(/\b(cat|dog)s\b/g)]- 结果
["cats","dogs"]- 换成
/\bcat|dogs\b/g ["cat","dogs"]
没有括号时,
|两侧各自吃掉它能吃到的一切,包括你以为是「公共部分」的\b和s。分支配上词边界,否则会在词内部命中
- 调用
"cat category dog".replace(/\b(cat|dog)\b/g, "pet")- 结果
"pet category pet"- 换成
/cat|dog/g "pet petegory pet"
批量替换是最容易出这个事故的地方 —— 它不报错,只是悄悄改坏了几个不该动的词。
练习
先自己写,再看答案。读懂和写得出是两件事,而这一节练的是后者。每道题的参考答案都由 npm run test:exercises-regex 实跑验证: 答案必须通过全部用例,「常见错解」必须至少被一条用例抓住, 而且 /.*/ 这类万能写法必须过不了 —— 否则这道题就没有区分度。
校验文件名以
.jpg、.jpeg或.png结尾用 re.test(输入) 判断
输入 期望 "a.jpg"应匹配 "a.jpeg"应匹配 "a.png"应匹配 "a.gif"不应匹配 "a.jpgx"不应匹配 提示
先把三种后缀圈成一组,再让
$管住整组。参考答案
/\.(?:jpe?g|png)$/🚨
|的优先级比锚点还低,不加括号时那个$只管住png这一支 ——a.jpgx会被放行。拿不准就加括号。常见错解
/\.jpe?g|png$/—— 它在"a.jpgx"这条上就错了。从
category里取出完整的category(同一条模式也要能取cat)用 输入.match(re) 取结果
输入 期望 "category"["category"] "cat"["cat"] "the category here"["category"] 参考答案
/category|cat/🚨 回溯引擎是「第一个能匹配的赢」,不是「最长的赢」。所以分支顺序有语义:长的、具体的放前面。⚠️ POSIX 那一系(
grep)规则相反,见[方言那篇](/regex/practice/dialects/)。常见错解
/cat|category/—— 它在"category"这条上就错了。校验:整串正好是
yes或no用 re.test(输入) 判断
输入 期望 "yes"应匹配 "no"应匹配 "yesno"不应匹配 "maybe"不应匹配 参考答案
/^(?:yes|no)$/不加括号时它读作
(^yes)|(no$)—— 放行任何以 yes 开头或以 no 结尾的串。这是|最经典的坑,而且对yes、no这两个测试输入都是对的。常见错解
/^yes|no$/—— 它在"yesno"这条上就错了。