1. 为什么正则表达式总是“一学就会,一写就废”
我接触 JavaScript 这么多年,几乎每个项目里都能碰到正则表达式。说句实话,这玩意儿是真难记,但也是真绕不开。表单验证要它、URL参数提取要它、日志分析要它、字符串清洗还要它。你去看招聘 JD,但凡写着“熟悉正则表达式”,基本就意味着你进了公司要处理各种乱七八糟的字符串需求。
但大多数人学正则的体验是这样的:看教程的时候觉得也就那么回事,\d匹配数字、\w匹配字母下划线,结果一到自己写就卡住了。尤其是写邮箱验证,网上复制一段正则,跑起来发现有的能过有的不能过,又不知道问题出在哪。再比如提取 URL 里的参数,很多人第一反应是用split('?')再接split('&'),但遇到 URL 编码、重复参数、特殊字符就直接翻车。
这篇文章我不会给你念文档,也不会罗列一堆背不完的元字符表。我只讲两个真实业务里最高频的场景——邮箱格式验证和URL参数提取,把正则表达式的核心逻辑拆开揉碎,再给出一套可以直接抄进项目的代码。适合刚入门 JavaScript 想搞懂正则的初学者,也适合写了好几年业务但正则始终靠百度的朋友。
先说一个心态问题。正则表达式不是背出来的,是“拆”出来的。你看到一个复杂正则第一反应不应该是“这谁写的,看不懂”,而是“它由哪几个组成部分拼接而成”。当你学会把复杂问题拆成小模块,正则基本上就通了一半。
2. 正则基础知识:不背语法,学会查表
2.1 字符匹配的底层逻辑
正则说白了就干一件事:按照某种规则,在字符串里找符合规则的内容。关键在于“规则”怎么描述。
我们人眼看到一串字符,比如abc123,能立刻告诉你有三个字母和三个数字。正则要表达这个意思,就需要有一套描述语言。这套语言里最小的单位叫“元字符”,你可以把每个元字符理解成一个“占位符”,它描述的是“这个位置应该是什么类型的字符”。
举个例子,\d表示“此处需要一个数字”,[a-z]表示“此处需要一个小写字母”,.表示“此处可以是任意字符”。当这些元字符组合起来,就形成了规则。比如\d{3}-\d{4}的意思是“三位数字、一个连字符、四位数字”,这正好匹配电话号码的中间段格式。
很多人记不住元字符,是因为硬背。我的方法是把它们分类理解:
- 字符类:
\d(数字)、\w(字母数字下划线)、\s(空白符)、.(任意字符)。这类元字符是“单个字符”的类型占位符。 - 量词:
*(0次或多次)、+(1次或多次)、?(0次或1次)、{n,m}(n到m次)。它们修饰前面那个字符或组出现的次数。 - 位置锚点:
^(字符串开头)、$(字符串结尾)、\b(单词边界)。它们不匹配任何字符,只匹配位置。 - 分组与逻辑:
(abc)分组、|或关系、[]字符集合。
这样一分,正则语法就有了骨架。你不需要背,只需要知道“描述一个位置用什么、描述次数用什么、描述范围用什么”,需要的时候查表即可。
2.2 贪婪匹配与懒惰匹配:最容易踩坑的隐蔽逻辑
这一小节必须单独讲,因为我在实际项目里见过太多人栽在这里。默认情况下,量词是“贪婪”的,也就是它会尽可能多地匹配字符。
举个例子,字符串是<div>内容1</div><div>内容2</div>,表达式<div>.*</div>会匹配到什么?新手以为是第一个<div>内容1</div>,其实是整个字符串,因为.*贪婪地把中间所有字符都吞了,直到最后一个</div>才停下。
如果只想匹配到第一个</div>就停,需要把量词变成“懒惰”的,写法是在量词后面加一个?,即<div>.*?</div>。这个?在这里是“懒惰标记”,不是“0次或1次”的意思。同一个符号,位置不同含义不同,这正是正则容易混乱的原因。
判断一个表达式是贪婪还是懒惰,就记住一句话:贪婪是往右吃到底,懒惰是吃到第一个能停的地方就停。实际工作中处理 HTML 片段、JSON 片段,绝大多数情况应该用懒惰匹配,否则你会拿到一堆不想要的内容。
2.3 分组捕获与反向引用:不只是为了“把结果包起来”
括号在正则里有两个作用:一是分组,把多个字符当成一个整体;二是捕获,把匹配到的内容单独存下来供后续使用。
比如提取 URL 参数时,我们需要把key=value中的 key 和 value 分别取出来,就要用括号:([^&=]+)=([^&=]+)。这两个括号里的捕获组,会在匹配成功后通过match结果的数组索引拿到具体值。
还有一种用法叫反向引用,指的是在正则内部引用前面已经捕获到的内容,用\1、\2表示第一组、第二组。比如判断一个字符串里是否有连续重复的字母,可以用([a-z])\1。这个技巧用在数据清洗、敏感词替换场景里非常实用,值得花点时间理解。
3. 邮箱验证实战:从“能用”到“够用”
3.1 先从业务需求出发,搞懂验证边界
我先问你一个问题:用户注册时邮箱验证到底要解决什么问题?
是想保证这个邮箱真实存在?做不到。正则能做的仅仅是“格式看起来合法”,比如abc@example.com这种结构。真实性的验证需要发激活邮件做回环测试,正则管不了。
是想完全匹配 RFC 5322 规范?理论上可以写一个巨长的、能匹配所有合法邮箱的正则,但那样做毫无意义。因为真正的用户不会给你提交那种极端格式的邮箱,反而可能因为正则太严格把正常邮箱拦在外面。
所以业务场景下的邮箱验证,核心目标就三条:
- 有
@符号,且@前后都有非空内容 @后面有域名部分,域名有点号分隔- 不包含明显的非法字符(空格、中文、连续点号等)
明白边界之后再写正则,思路就清晰了:验证的是“看起来差不多像个邮箱”,不是“全宇宙最严谨的邮箱格式”。
3.2 一步步拆解一个可用的邮箱正则
我不用那些网上流传的、上百个字符的“严格模式邮箱正则”,那种正则既难维护又难理解。我自己在项目里常用的是下面这个:
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;别急着直接用,先看我拆解:
^:匹配字符串开头,表示后面必须从第一个字符开始[a-zA-Z0-9._%+-]+:邮箱的 local 部分(@ 前面),允许字母、数字、点、下划线、百分号、加号、连字符,出现一次或多次。+表示至少一个字符@:字面量,邮箱必须有 @[a-zA-Z0-9.-]+:域名主体部分,允许字母、数字、点、连字符\.:注意这里有个反斜杠,点号在正则里是“任意字符”的意思,要匹配真正的点号必须转义[a-zA-Z]{2,}:顶级域部分,比如 com、cn、org 都是 2 个字母以上$:字符串结尾,保证整个字符串都参与匹配
拆完你会发现,这个正则翻译成人话就是:“以非空的合法字符开头,中间有个 @,@ 后面是域名和顶级域,最后以顶级域结束”。逻辑清晰,每部分都能解释,出了问题也能快速定位。
3.3 为什么不能用.*@.*这种简单写法
有些偷懒的写法长这样:/.*@.*/,意思是“任意字符 + @ + 任意字符”。这能通过a@@b这样的输入,因为它只要求有 @ 就行,不管 @ 前后是不是合法格式。
还有些人用/^\S+@\S+\.\S+$/,意思是“非空非空 @ 非空非空点非空”。比.*@.*好一点,但\S匹配任何非空白字符,会放过a@b..c、a@b.这种残缺格式。问题就出在“太宽松”。
过度宽松的正则在业务中的隐患是:用户填了abc@def,前端提示“格式正确”,结果后端发邮件一直失败,用户等半天收不到验证码,最后流失。这种体验层面的损失,远比多写几个字符的代价大。
3.4 加入 JavaScript 方法封装与异常处理
正则只是规则,真正使用时要结合 JS 方法。我一般写成一个独立的校验函数,方便在多个表单里复用:
function isValidEmail(email) { const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (typeof email !== 'string') { return false; } if (email.length > 254) { return false; } return emailRegex.test(email.trim()); }这里有三个细节值得说:
第一,先判断传入类型是不是字符串。JS 是弱类型语言,接口返回的数据字段有可能是null、undefined、数字,直接调用.test()可能会隐式转换类型,导致意外结果。
第二,加了长度判断。RFC 标准里邮箱总长度上限是 254 个字符。虽然正常人用不到那么长,但防止有人故意塞一大段字符串进来,后端接口会有传输压力。前端拦一道不用花钱,但能省后面的麻烦。
第三,使用了.trim()去掉首尾空格。用户从输入框复制邮箱经常带前后空格,如果不去掉,正则必然匹配失败,用户会以为系统坏了。这是我在真实项目中踩过坑之后的教训。
关于test()方法有一个隐藏的坑:如果正则表达式带了全局标志g,反复调用test()会因为你上次匹配到的位置不同而得到不同结果。用test()验证邮箱时不要加g标志,否则第二次校验同一个邮箱可能神奇地返回 false。
4. URL参数提取:从“能用”到“抗造”
4.1 你写的第一个提取函数大概率有 bug
URL 参数提取是前端里非常常见的需求。比如拿到一个分享链接,要读取?id=123&name=lucy里的参数;比如做活动页埋点,要统计utm_source、utm_medium这些渠道参数;再比如处理支付回调,要解析orderId、sign等参数。
很多新手第一次写提取函数是这样的:
function getParam(url, key) { const arr = url.split('?')[1].split('&'); for (let i = 0; i < arr.length; i++) { const kv = arr[i].split('='); if (kv[0] === key) { return kv[1]; } } }这个函数在“参数规规矩矩”的情况下能跑通,但真实世界的 URL 没这么讲道理。下面这些情况它都处理不了:
- URL 里没有问号,直接
arr[1]报错 - 参数值里有
=号,比如name=a=b,split('=')会得到三段,kv[1]只有a - 参数值经过 URL 编码,比如名字是“张三”,传到 URL 里就变成了
%E5%BC%A0%E4%B8%89,直接拿回来没法用 - 同一个 key 出现多次,比如
?tag=a&tag=b,传统写法读出来只有最后一个 - URL 里有 hash,比如
example.com/path?id=1#/home,hash 部分被吞进参数值里
4.2 正则提取 URL 参数的完整方案
先明确一点:浏览器环境里原生有URLSearchParamsAPI 可以用:
const params = new URLSearchParams('?id=1&name=lucy'); params.get('name'); // 'lucy'但如果你在 Node.js 环境里的旧版本、或者需要处理一些特殊场景,或者你想彻底搞懂底层原理,正则仍然是有价值的。而且面试的时候,手写 URL 参数解析几乎是必考题,只回答“用 URLSearchParams”会给面试官留下“知其然不知其所以然”的印象。
用正则提取 URL 参数,核心表达式是这样的:
/([^?&=]+)=([^?&=]*)/g拆解一下:
([^?&=]+):第一个捕获组,匹配参数名。[^?&=]表示“不是问号、不是与符号、不是等号的任意字符”,+表示至少一个。这样可以确保参数名不会把分隔符吃掉=:字面量等号([^?&=]*):第二个捕获组,匹配参数值。用*而不是+,是因为有些参数可能没有值,比如?flag&id=1,flag的值是空字符串/g:全局匹配,把 URL 里所有参数都找出来
有了正则,再配合matchAll或exec循环就能拿到所有参数:
function parseUrlParams(url) { const params = {}; const regex = /([^?&=]+)=([^?&=]*)/g; const matchAll = url.matchAll(regex); for (const match of matchAll) { const key = decodeURIComponent(match[1]); const value = decodeURIComponent(match[2]); params[key] = value; } return params; }注意matchAll要求表达式必须有g标志,否则会抛异常。然后我用decodeURIComponent对 key 和 value 都做了一次解码,这一步很关键。如果 URL 里的值是中文编码成%E5%BC%A0%E4%B8%89,不解码拿到的就是乱码,业务里根本没法用。
4.3 如何处理重复参数与哈希串
上面这个函数能解决 90% 的问题。但如果遇到?tag=a&tag=b这种重复参数,直接赋值params['tag'] = value会把前面的覆盖掉。业务里重复参数很常见,比如多选筛选条件?category=book&category=phone。
我的做法是:检测到 key 已存在时,把值聚合成数组:
function parseUrlParams(url) { const params = {}; const regex = /([^?&=]+)=([^?&=]*)/g; const matches = url.matchAll(regex); for (const match of matches) { const key = decodeURIComponent(match[1]); const value = decodeURIComponent(match[2]); if (Object.prototype.hasOwnProperty.call(params, key)) { if (Array.isArray(params[key])) { params[key].push(value); } else { params[key] = [params[key], value]; } } else { params[key] = value; } } return params; }这段代码判断 key 是否已存在,如果存在且已经不是数组,就把原来的值和新值合成数组;如果已是数组,直接 push。逻辑不复杂,但考虑得全面,放到生产环境里不容易出幺蛾子。
再说 hash 的问题。假设 URL 是https://example.com/page?id=1#/home/abc,正则里的[^?&=]+不会匹配到#和/,所以它能正常解析出id=1,#/home/abc会被忽略。但如果是?redirect=/path?from=page这种嵌套结构,参数值里的?会导致解析错乱。这种极端情况就不建议用正则硬解了,要么约定好编码方式,要么换更强大的 URL 解析库。
4.4 从URL中提取单个参数的轻量函数
有时候我们只需要取单个参数,不需要完整解析所有参数。那可以写一个更轻量的版本:
function getUrlParam(url, key) { const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp('[?&]' + escapedKey + '=([^&#]*)'); const match = url.match(regex); return match ? decodeURIComponent(match[1]) : null; }这段代码里有三个值得学习的点:
第一,new RegExp()动态构造正则。当 key 是变量时,不能直接写/.../字面量,必须用构造函数动态生成。
第二,escapedKey做了特殊字符转义。如果 key 本身包含.、*、?这类正则保留字符,不对它转义的话正则就会被破坏。这里的写法是标准做法,把 14 个特殊字符全部加上反斜杠。
第三,表达式开头用[?&]而不是直接用?。这样保证匹配到的 key 一定是参数边界,不会把别的参数值里的相似文本误匹配。比如?name=abc&nickname=lucy,如果要取name,直接写name=会把nickname里的name=匹配到。加一个[?&]就能避免这个问题,因为nickname前面是&,但更关键的是正则继续向后匹配要去掉其他字符。
还有一个细节,([^&#]*)这个捕获组把值的范围限定为“不是 & 不是 # 的任何字符”,这样参数值不会吞掉 hash 部分。match方法返回的数组里,match[1]就是第一个捕获组的内容,也就是参数值。
4.5 用URLSearchParams做兜底对比
我前面强调正则是有用的,但不代表要排斥原生 API。真实项目里我的建议是:
- 如果只处理当前浏览器窗口的 URL,用
new URLSearchParams(window.location.search)最省事 - 如果处理的是任意一段 URL 字符串,且需要兼容老环境,用正则方案
- 如果参数结构非常复杂(嵌套对象、数组、特殊编码),建议用成熟的库如
qs或query-string
URLSearchParams和正则方案的主要区别在于:它严格遵循 WHATWG 规范,对+号会解析成空格(因为 URL 编码规范里+表示空格),而正则方案如果不手动处理,会把+原样返回。从 W3C 标准的角度,URLSearchParams更准确;但很多内部约定直接往 URL 里塞+作为加号,这种场景正则方案反而更直观。两者没有绝对优劣,选合适你业务的就行。
5. 常见问题与调试技巧:正则写错了,你是怎么发现的
5.1 三个高频报错场景与定位方法
第一类是“整个表达式匹配不到任何东西”。这种问题通常出在量词用错了位置,比如[a-z]{2,}写成了[a-z]2,,少了花括号,表达式意思就变了。排查方法是把正则分段测试,先测第一段能不能匹配,再逐步加长。
第二类是“匹配到的内容比预期多”。这是贪婪匹配搞的鬼,解决方法是给量词加?。比如解析 HTML 里的属性,href=".*"可能一直匹配到最后一个引号,而href=".*?"只匹配到第一个引号。
第三类是“正则里写了中文或特殊字符导致报错”。JavaScript 正则字面量里能直接写中文,但如果你用new RegExp()动态拼接,要注意字符串转义。比如要匹配反斜杠\,正则字面量里写/\\/,字符串里要写'\\\\',这里面的层级关系很容易搞混。
排查正则有一个好习惯:用可视化工具把表达式转成图形,比如 RegExr、regex101,它们会把分组、量词、字符类用不同颜色标出来,一眼就能看出哪里结构不对。我每次写复杂正则都会先丢进工具里验证再上代码,能省很多调试时间。
5.2 全局匹配 g 标志引发的隐蔽 Bug
前面提到过,用test()配合带g的正则会有问题。我再详细解释一下,因为这是真实项目里最隐蔽的坑。
看这段代码:
const regex = /^\d+$/g; console.log(regex.test('123')); // true console.log(regex.test('123')); // false ?!同一个正则,同一个字符串,两次结果不一样。原因是带g的正则对象是有状态的,它有一个lastIndex属性,记录上一次匹配结束的位置。第一次test成功匹配后,lastIndex变成了 3(表示从索引 3 开始继续);第二次test从索引 3 开始匹配,但字符串长度为 3,且结尾锚点$要求必须匹配到末尾,所以直接失败。
解决方案:要么去掉g标志,要么每次手动重置regex.lastIndex = 0。用matchAll时也必须注意,它迭代完一次后,如果再调用需要重新创建正则对象,否则也会因为lastIndex停留在末尾而返回空。
这也是为什么我前面写的邮箱验证正则不带g。任何“单次完整匹配”的场景,都不要加g。
5.3 正则表达式性能优化:回溯灾难怎么避免
很多新手没意识到正则可能会有性能问题。当你写的表达式和文本发生大量匹配失败时,正则引擎会不断尝试不同的匹配路径,这叫做“回溯”。极端情况下回溯次数呈指数级增长,CPU 被打满。
最典型的性能杀手是(a+)+$这种“嵌套量词”模式。假如字符串是aaaaaaaaaab,引擎会尝试各种拆分方式,直到耗尽所有可能才发现匹配失败。这个过程中计算量大到能卡死页面,这在哈希算法里被称为“ReDoS(正则表达式拒绝服务攻击)”。
日常开发里的建议是:
- 能不用嵌套量词就不用,比如
(a+)+改写成a+ - 用非贪婪
*?时要注意,它不一定比贪婪更快,只是匹配结果不同 - 对不确定长度的输入,用
indexOf配合slice做前置过滤,减少正则进入概率 - 如果有大批量字符串需要校验(比如一次验证几万条邮箱),用正则
.test()的效率远高于new RegExp()每次创建
5.4 常用正则速查表:写代码之前先来这里找
下面这张表是我自己电脑上一直存着的,几乎覆盖了 80% 的日常场景,直接抄走即可:
| 场景 | 正则表达式 |
|---|---|
| 手机号(中国大陆) | /^1[3-9]\d{9}$/ |
| 身份证号(18位) | /^\d{17}[\dXx]$/ |
| IP地址(IPv4) | /^((25[0-5] | 2[0-4]\d | 1\d{2} | [1-9]?\d)\.){3}(25[0-5] | 2[0-4]\d | 1\d{2} | [1-9]?\d)$/(实际使用要拆分组) |
| 日期 YYYY-MM-DD | /^\d{4}-(0[1-9] | 1[0-2])-(0[1-9] | [12]\d | 3[01])$/ |
| 16进制颜色 | /^#?([0-9a-fA-F]{6} | [0-9a-fA-F]{3})$/ |
| 整数 | /^-?\d+$/ |
| 浮点数 | /^-?\d+\.?\d+$/ |
| URL | /^https?:\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/ |
| 中文 | /^[\u4e00-\u9fa5]+$/ |
| 用户名(字母开头,4-16位) | /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/ |
注意手机号正则里的\d{9}是 9 位还是 8 位要确认,1[3-9]\d{9}一共是 11 位数字。如果写成分组1[3-9]\d{9}中的\d{9}表示 9 位数字,总计 11 位没问题。身份证号的正则只校验了格式和校验位符号,不能保证身份证真实有效。
5.5 正则替换与回调函数的配合使用
除了验证和提取,正则还有一个高频功能就是替换。String.prototype.replace()支持传入回调函数,这为文本处理打开了很大的想象空间。
比如有一串文本里所有的数字都要加 1:
const text = '今年是2024年,去年是2023年'; const result = text.replace(/\d+/g, (match) => { return Number(match) + 1; }); // 今年是2025年,去年是2024年回调函数的第一个参数match是匹配到的完整内容,如果不带g则只替换第一个匹配项。回调里可以做任何逻辑,比如查表映射、调用接口、拼接字符串,比单纯的替换字符串强大太多。
再比如从一段 HTML 里批量提取所有图片的 URL:
const html = '<img src="https://example.com/a.jpg"><img src="/b.png">'; const urls = []; const regex = /<img[^>]+src=["\']([^"\']+)["\']/g; let match; while ((match = regex.exec(html)) !== null) { urls.push(match[1]); }这里用了exec循环而不是matchAll,因为exec在循环里每执行一次会更新lastIndex,配合while反复调用直到返回null,这样能拿到每个匹配的捕获组。两种方式等效,看个人习惯。
6. 进阶思路:正则在前端工程化里的更多用途
6.1 用正则做数据脱敏与隐私保护
很多页面需要在展示用户信息时隐藏敏感字段,比如手机号中间四位用星号代替。正则做这种脱敏极其高效:
function maskPhone(phone) { return phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'); } maskPhone('13812345678'); // 138****5678这里用到了替换字符串里的$1、$2,分别指向第一个和第二个捕获组。逻辑是:捕获前三位和后四位,中间四位用星号替换。同理还可以脱敏邮箱:
function maskEmail(email) { return email.replace(/^(.)(.*)(@.*)$/, '$1***$3'); } maskEmail('zhangsan@example.com'); // z***@example.com捕获组里的.只匹配一个字符,(.*)匹配中间任意长度,最后(@.*)把 @ 及域名部分保留。这种脱敏在管理后台、日志展示、异常信息上报时非常常用,但要注意脱敏逻辑本身可能被绕过,不能把它当作安全机制,它只是“降低敏感信息泄露的影响”。
6.2 用正则做路由匹配与权限校验
前端的单页应用里,路由匹配也可以依赖正则。Vue Router 的路径参数就支持正则模式,React Router 的path-to-regexp底层也是正则思路。
比如你想匹配/user/:id但限制 id 必须是数字,可以使用通配符写法,转换成正则后是^/user/(\d+)$。这种场景下,正则能把“路由约束”和“参数校验”一次做完,省去后续在组件里再判断一次类型。
权限校验也经常用正则:比如判断当前跳转的路径是否在某个受保护前缀下。/^\/admin\//能匹配所有/admin/开头的路径。如果还需要排除某些公开页面,可以配合(?!...)负向前瞻来做排除。这类技巧在封装路由守卫时比较有用。
6.3 用正则处理日志上报与信息过滤
前端错误监控里,我们需要从堆栈信息中提取关键字段。堆栈文本通常是这种格式:
at Object.getUserInfo (http://example.com/js/app.12345.js:120:30)要提取出文件地址、行号、列号,可以这样写:
const stackLine = 'at Object.getUserInfo (http://example.com/js/app.12345.js:120:30)'; const regex = /(https?:\/\/[^\s]+):(\d+):(\d+)/; const match = stackLine.match(regex); if (match) { const [, url, line, column] = match; console.log(url, line, column); }(https?:\/\/[^\s]+)匹配 http 或 https 开头、不包含空白字符的地址;:(\d+)匹配冒号后的行号;再一个:(\d+)匹配列号。配合解构赋值,三行代码拿到核心信息。这套方案在自研前端监控平台时特别实用。
信息过滤场景更常见。前端聊天室、评论区要屏蔽违禁词、广告词,用正则做敏感词替换是最直接的方案。写一个简单的多词替换函数:
const bannedWords = ['垃圾', '广告', '诈骗']; const regex = new RegExp(bannedWords.join('|'), 'g'); const filtered = input.replace(regex, '***');这种方式在词表量级小的时候完全够用。如果词表达到几千条,建议用 Trie 树或者其他数据机构做优化,否则正则回溯性能会出问题。
7. 我在实际项目里总结的小技巧
用了这么多年正则,说几个不一定写在文档里、但实战特别好用的小技巧。
第一个,能用字符串方法解决的,不要强行上正则。如果只是判断字符串是否包含某个子串,includes就够了;如果只是按固定分隔符拆数组,split更清晰。正则不是万能的,滥用正则会让代码变得难读。
第二个,写正则的时候永远想着“这个表达式的边界是什么”。比如验证邮箱,你想的是“@ 前面必须有内容”,那边界就是“至少一个字符”。再比如提取 URL 参数,你想的是“参数名不能包含分隔符”,那边界就是[^?&=]。边界想清楚了,正则就不会写出模糊匹配。
第三个,任何写进业务代码的正则,都要配测试用例。正则表达式是出了名的“写的时候爽,改的时候疼”,没有测试用例兜底,你根本不知道加一个字符会不会影响之前的匹配行为。至少把正常的输入、边界输入、异常输入都给覆盖到。
第四个,命名捕获组,提升可读性。JavaScript 支持命名捕获组,写法是(?<name>pattern),匹配结果可以通过match.groups.name直接取。这在捕获组多的时候特别有用。
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const result = '2025-06-18'.match(regex); console.log(result.groups.year); // 2025 console.log(result.groups.month); // 06 console.log(result.groups.day); // 18以前取捕获组是靠下标match[1]、match[2],一多就容易记混。现在有了命名组,代码一眼就能看懂,强烈建议使用。
最后一个也是我认为最重要的:正则表达式是写给下一个维护者看的,不是写给你自己爽的。你再会的语法,三个月后也会忘。所以复杂正则一定要写注释,把每段表达式的作用用//或#标注在旁边。如果项目里能接受,甚至可以专门抽一个regex.js文件,把所有业务正则集中管理,每个正则旁边写上匹配示例和设计思路。这样一来,后来接手的同事就不用对着天书发愁了。
正则这个东西,你花一周背语法,不如花一天真正解决一个实战问题。希望这篇文章里邮箱验证和 URL 参数提取两个案例的拆解,能帮你建立起“从需求到表达式”的思维路径。下次再拿到提取、验证、替换类需求,先别急着百度,拿出纸笔梳理一下边界条件,你会发现正则其实没那么难。