打底:它到底在做什么

10 分钟写出你的第一条正则

这一篇不解释「为什么」

后面十一篇都在解释为什么。这一篇只有一个目的:让你在十分钟内写出一条能用的正则, 并且知道它为什么能用。

任务:校验一个 11 位手机号(以 1 开头,后面十位数字)。

你需要认识的东西一共四个,一个都不多。

第 1 步:先写死

正则里,普通字符就匹配它自己。所以这已经是一条合法的正则了:

/13800138000/

它能匹配 13800138000。问题也很明显——它对别的号码一律说不。

但这一步不是白写的:它确立了一件事,正则是「一个模板」,从左到右逐个字符比对。 接下来要做的只是把「会变的那部分」换成能代表一类字符的写法。

第 2 步:把会变的部分换掉

手机号里变的是后面那十位数字。「一个数字」写作 \d:

/1\d\d\d\d\d\d\d\d\d\d/

能用,但难看。「重复十次」写作 {10}:

/1\d{10}/

到这里你已经认识三个东西了:普通字符、\d、{n}。 ^1\d{10}$ 匹配 13900139000,而少写一位的 ^1\d{9}$ 不匹配—— 说明 {10} 是精确的十次,不是「大约十位」。

(先别管那两个 ^ $,下一步就是它们。)

🚨 第 3 步:说明「整串就这么长」

这是最关键的一步,也是新手最常漏的一步。

/1\d{10}/ 有个要命的问题:它问的是「这串里含不含一个 11 位手机号」, 而不是「这串是不是一个手机号」。于是:

/1\d{10}/ 会放行 '13800138000abc'。

在一个注册表单里,这意味着用户能提交任何以手机号开头的垃圾。

解决办法是在两端各加一个符号:

^   表示「字符串从这里开始」
$   表示「字符串到这里结束」
/^1\d{10}$/     // ← 这就是最终答案

加上之后,'13800138000abc' 就过不了了:^ 要求从头开始、$ 要求到此为止, 中间只允许 1 加十位数字,多一个字符都不行。

👉 把这条记成习惯:写校验类正则,先写 ^ 和 $,再往中间填东西。 它能挡掉你未来一半以上的 bug。

跑一遍

打开浏览器控制台(F12),粘贴:

const PHONE = /^1\d{10}$/;

PHONE.test('13800138000')      // 你觉得是什么?
PHONE.test('13800138000abc')
PHONE.test('1380013800')

⚠️ 真的去跑一遍。 这一步不能用「我看懂了」代替—— 正则是一种写的技能,而写的技能只能靠手熟。

你现在会的四个东西

写法 意思
abc 就是这三个字符
\d 任意一个数字(0-9)
{10} 前面那个东西重复 10 次
^ $ 字符串的开头 / 结尾

就靠这四个,你已经能写出不少实用的校验了——下面的练习就是。

一个现在就该知道的坑

\d{10} 里的 {10} 管的是它左边紧挨着的那一个东西,也就是 \d, 不是「前面那一串」。所以:

1\d{10}      1,然后十个数字            ← 对
(1\d){10}    「1 加一个数字」重复十次    ← 完全是另一回事

这条规则后面会反复用到(量词只管紧挨着的一个元素),现在只要记住 「重复」永远只作用于紧挨着的那一个就够了。

接下来读什么

你现在能写简单的校验了,但还不知道它为什么这样工作—— 于是遇到稍微复杂一点的情况就会卡住,比如:

  • 为什么我的正则匹配到了一大坨我不想要的东西?
  • 为什么同样一条正则,在别人的代码里能跑、粘到我这儿就不行?

《正则引擎在干什么》 回答第一类问题。 它讲的模型很小——引擎只做三件事——但后面所有的「坑」都是那三件事的推论。 先花十分钟把它读完,比先背五十个符号有用得多。

⚠️ 但在那之前,先把下面的练习做了。三道题,都只用到这一篇教的四个东西。

本篇示例

下面每一条都由 npm run test:regex 在每次构建前实跑验证, 结果是现算的,不是抄进数据里的副本。正文里的代码块只用来演示匹配过程和写法对照,不进闸门; 凡是「这个模式配这个输入得到这个结果」的断言,只存在于这里。

  1. 写死的模式只匹配它自己

    调用
    /13800138000/.test("13900139000")
    结果
    false
    换成 /1\d{10}/
    true

    这就是「写死」的全部问题:它对别的手机号一律说不。

  2. \d{10} = 十个数字

    调用
    /^1\d{10}$/.test("13900139000")
    结果
    true
    换成 /^1\d{9}$/
    false

    \d 是「一个数字」,{10} 是「重复十次」。手机号首位固定是 1,所以写成 1 加十个 \d。

  3. 🚨 不加 ^ $,后面跟一坨垃圾也能过

    调用
    /^1\d{10}$/.test("13800138000abc")
    结果
    false
    换成 /1\d{10}/
    true

    ⚠️ 校验类正则必须两端锚定。这是全书最常见的一个坑,也是你现在就该养成的习惯。

练习

先自己写,再看答案。读懂和写得出是两件事,而这一节练的是后者。每道题的参考答案都由 npm run test:exercises-regex 实跑验证: 答案必须通过全部用例,「常见错解」必须至少被一条用例抓住, 而且 /.*/ 这类万能写法必须过不了 —— 否则这道题就没有区分度。

  1. 校验一个六位数字的短信验证码:整串必须正好是 6 个数字

    用 re.test(输入) 判断

    输入期望
    "123456"应匹配
    "12345"不应匹配
    "1234567"不应匹配
    "abc123"不应匹配
    "123 456"不应匹配
    提示

    先写「六个数字」,再想「怎么说明整串就这么长」。

    参考答案

    /^\d{6}$/

    少了 ^ 和 $,问的就变成「串里含不含六位数字」—— 七位数字的串里当然含。校验类正则必须两端锚定。

    常见错解 /\d{6}/ —— 它在"1234567"这条上就错了。

  2. 校验 QQ 号:5 到 11 位数字

    用 re.test(输入) 判断

    输入期望
    "12345"应匹配
    "12345678901"应匹配
    "1234"不应匹配
    "123456789012"不应匹配
    "1234a"不应匹配
    提示

    {n,m} 表示「n 到 m 次」。注意逗号后面不能有空格。

    参考答案

    /^\d{5,11}$/

    半锚定是最危险的写法 —— 它对合法输入全对,只在「前缀合法、后面还有东西」时失效,而那恰恰是被人构造出来的那一类输入。

    常见错解 /^\d{5,11}/ —— 它在"123456789012"这条上就错了。

  3. 校验日期格式 2026-09-11:四位年、两位月、两位日,中间用连字符

    用 re.test(输入) 判断

    输入期望
    "2026-09-11"应匹配
    "2026-9-11"不应匹配
    "20260911"不应匹配
    "2026-09-11x"不应匹配
    参考答案

    /^\d{4}-\d{2}-\d{2}$/

    连字符是普通字符,直接写即可。⚠️ 但这条只管格式:2026-13-45 也会通过 —— 值合不合法得交给 Date 判,见[实战那篇](/regex/practice/validate-extract-replace/)。

    常见错解 /^\d{4}-\d{2}-\d{2}/ —— 它在"2026-09-11x"这条上就错了。