写 JavaScript 这么久,正则表达式是我觉得最“玄学”的一块。你说它难吧,真正吃透那几十个符号之后,很多字符串处理的活儿能瞬间从半小时压缩到一行代码;你说它简单吧,一碰到复杂匹配,[0-9]和\d的区别、贪婪与非贪婪、前瞻后顾……每一个点都能把人绕晕。这篇文章就是要把这些“玄学”拆开,用前端开发最常见的表单校验、数据清洗场景,把 JavaScript 正则表达式的语法、API、实战套路一次讲清楚。不管你是刚入门的前端新手,还是写了两三年业务代码但正则一直靠粘贴复制的同学,这篇都能帮你补齐这块短板。
1. 正则表达式到底是什么:先建立“匹配”的思维模型
1.1 正则不是代码,是一种“描述规则”的语言
很多人第一次接触正则,会下意识地把它当成某种“高级代码”来背,符号记了一大堆,用的时候还是懵。我建议换个角度理解:正则表达式本质上不是一段执行逻辑,而是一种描述字符串“长相”的规则语言。你写的每个模式,都是在告诉 JavaScript:我需要找出满足这个形状特征的字符串。
打个比方,你去图书馆找书,管理员问你“找什么书”,你说“书名以 Java 开头、中间任意字符、最后是开发指南”,管理员就能按这个描述去书架上筛。正则干的就是这件事,/^Java.*开发指南$/就是一条更精确、更机器化的描述。区别在于,图书馆管理员有理解能力,正则是纯字面规则,所以你必须在每个细节上都说清楚:开头是什么、结尾是什么、中间允许多少字符、字符的取值范围是什么。
这也解释了为什么正则初看很难——它和普通命令式编程的“先做 A 再做 B”不太一样,它是声明式的:你在描述“我要什么样的字符串”,而不是“怎么一步步去找”。一旦把思维从“写步骤”切换到“描述形状”,后面学语法就容易多了。
1.2 字符类与预定义类:把“任意字符”说清楚
正则最基础的元素是字符类。最直接的写法就是字面量字符,/abc/匹配字符串里恰好abc这三个连续字符。但现实中更多需求是“匹配某一类字符”,比如“匹配一个数字”,这时就需要字符类。
方括号[ ]是最直观的字符类写法:[0-9]表示任意一个数字,[a-zA-Z]表示任意一个英文字母,[^0-9]表示任意一个非数字。注意这里的^在方括号开头才表示“非”,放在别的位置就是另一个含义了,这个细节经常有人踩坑。
除了自己写范围,JavaScript 还提供了几个预定义类,用的人最多也最实用:
\d:等价于[0-9]\D:等价于[^0-9]\w:等价于[A-Za-z0-9_],注意包含下划线\W:等价于[^A-Za-z0-9_]\s:匹配空白字符,包括空格、Tab、换行\S:匹配非空白字符.:匹配任意字符,但默认不匹配换行符
我把这堆符号当成“快捷指令”来记,上手会快很多。比如要匹配一个手机号,不用写[0-9]这种长串,直接\d就行。但要记住:\w和\d默认都是基于 ASCII 的,\w并不包含中文,很多前端新手在匹配中文时发现\w不生效,根本原因就在这。后面我会专门讲中文和 Unicode 的处理。
1.3 量词与贪婪模式:匹配次数怎么控
光有字符类还不够,你只能匹配“一个”字符。真实场景里,手机号是 11 位数字、用户名长度是 3 到 16 位,这就需要用量词来控制出现次数。
量词总共有这么几个:
*:前一个字符出现 0 次或多次,等价于{0,}+:出现 1 次或多次,等价于{1,}?:出现 0 次或 1 次,等价于{0,1}{n}:恰好出现 n 次{n,}:至少出现 n 次{n,m}:出现 n 到 m 次
量词的另一个重要概念是贪婪与非贪婪。默认情况下,量词都是贪婪的:它会在保证整体匹配成功的前提下,尽可能多地匹配字符。举一个经典例子:如果用/<.*>/去匹配<b>标题</b>,结果不是匹配到<b>和</b>,而是直接吞掉整行,因为.*会“贪”到最后一个>才停。
想让它“见好就收”,就在量词后面加一个?,变成非贪婪模式:/<.*?>/就能分别匹配出<b>和</b>,因为在第一个>出现时它就停手了。这个知识点在解析 HTML 标签、配置项提取时几乎必用,前端面试也常考,一定要搞明白。
2. JavaScript 里正则的三种写法和 API 全景
2.1 字面量、构造函数与 RegExp 对象
前端日常写正则,最常见的写法是字面量:以斜杠包裹模式,后面可选跟修饰符。比如/^\d{11}$/g。字面量写法简单直接,性能也好,因为它在脚本加载时就被编译了,不会每次运行都重新解析。
另一种写法是用new RegExp('模式字符串', '修饰符')。这里有个容易翻车的点:模式字符串里的反斜杠本身需要转义。比如字面量里写\d表示数字,如果放到构造函数里就要写成"\\d",因为第一个反斜杠是在给字符串字面量转义。很多人直接new RegExp('^\d{11}$'),到浏览器里一看,模式变成了匹配字母d而不是数字,就是因为少了这层转义。
动态正则场景才是构造函数的主场。最典型的是搜索关键字:用户输入一个词,你想把内容里所有出现该词的地方高亮。这时模式里包含用户输入,就只能拼接字符串再传给RegExp,比如new RegExp(\(${keyword})`, 'g')。但要注意,用户输入里如果带了正则特殊字符会破坏整个模式,需要先做一个转义函数,把.、*、?等字符前面加上反斜杠,再拼接。这段经验我在项目里踩过几回,没有转义时用户搜a.b`,结果匹配了一堆奇怪内容,页面高亮全乱了。
2.2 核心方法:test、exec 与字符串方法
正则对象和字符串对象之间有几个互相配合的方法,新手的困惑点主要是它们分别该用谁。我建议按需求分:
- 只想判断“有没有匹配”,用
regex.test(str),返回布尔值 - 想拿到匹配到的内容、捕获组、位置信息,用
regex.exec(str)或str.match(regex) - 要替换,用
str.replace(regex, replacement) - 要找位置,用
str.search(regex) - 要按正则切分字符串,用
str.split(regex)
其中最容易踩坑的是match。str.match(regex)在不带g修饰符时,返回一个数组,包含第一个完整匹配、各个捕获组、index和input等属性;一旦带上g,返回的数组里就只有所有完整匹配的字符串,捕获组信息全部丢失。同样的正则、同一个字符串,带上g结果结构差别很大,这在处理数据时很容易造成 bug。
exec则和g绑定得很紧:循环调用exec时,正则会从上次的lastIndex继续往后找,直到返回null。这个特性可以用来遍历提取所有匹配,但如果你没有用循环、只在g模式下调用了一次exec,它只会返回第一个匹配,这跟“全局匹配”的直觉是相悖的。我见过不少同事在这里写错,所以统一建议:只需要第一处匹配时不加g;需要全部结果时要么用match + g,要么老老实实写exec循环。
2.3 修饰符 g、i、m、s、u、y 的含义与坑点
修饰符是正则的“开关”,JS 里一共有 6 个:g、i、m、s、u、y。
g是全局匹配,i是忽略大小写,这两个没啥可说的。m是多行模式:默认情况下^和$匹配的是整个字符串的开头和结尾,加了m之后,它们会匹配每一行的行首行尾。比如处理多行文本里以 TODO 开头的行,/^TODO/gm就能把每一行开头的 TODO 都抓到。
s修饰符是 ES2018 才加的,只干一件事:让.能匹配换行符。在它出现之前,想匹配“任意字符包括换行”,只能写成[\s\S]这种别扭写法。所以现在看到[\s\S]*?的老代码,心里要有数,那是在模拟s模式。
u是 Unicode 模式,处理中文、emoji 时很重要。加了u之后,正则可以用\u{1F600}这类形式直接匹配码点,也能正确处理四字节的 emoji 字符,否则\w和.在 emoji 面前会出问题。这个后面展开讲。
y是粘性匹配,比较少见。它和g类似依赖lastIndex,但区别是y要求匹配必须从lastIndex位置开始,不能跳过中间字符。做词法分析、解析器时会用到,业务开发基本用不上,了解一下即可。
3. 正则语法拆解:分组、捕获与零宽断言
3.1 分组与捕获:用小括号圈出关键信息
小括号在正则里有两种作用:分组和捕获。分组就是把多个字符当做一个整体来应用量词,比如/(ab)+/匹配ab、abab、ababab。捕获则是把匹配到的子内容存下来,方便后续引用。
捕获组从 1 开始编号,在replace里用$1、$2引用,在正则内部用\1、\2反向引用。举个例子:想匹配重复出现的单词,比如“hello hello”,可以用/(\w+)\s\1/,这里的\1表示“和第一个捕获组一样的内容”。反向引用是处理重复模式的利器,比如匹配成对标签、引号内的内容。
如果只是想把几个字符圈起来应用量词,但不想把内容存下来,就用非捕获组(?:...)。这样既能分组,又不会多出捕获组编号,对exec的结果结构也更干净。命名捕获组(?<name>...)是 ES2018 的新特性,可以给捕获组起名字,在replace里用$<name>引用,在exec结果里通过groups.name取值。我强烈建议复杂正则里优先用命名捕获组,否则$1、$2多了真的会看花眼。
3.2 前瞻与后顾:零宽断言都在判断“后面/前面是什么”
前瞻和后顾是正则里最有意思也最容易被忽略的能力,它们统称零宽断言。零宽的意思是:它们本身不消费字符,只做位置的“检查”。就像过关卡时不拿东西,只是递给保安看一眼证件,然后继续往前走。
JS 支持四种断言:
(?=...)正向前瞻:右边必须匹配...(?!...)负向前瞻:右边必须不匹配...(?<=...)正向后顾:左边必须匹配...(?<!...)负向后顾:左边必须不匹配...
以数字千分位格式化为例:要给1234567.89加上千分位逗号,核心正则就可以写成/\B(?=(\d{3})+(?!\d))/g。这里的\B是“非单词边界”,(?=(\d{3})+(?!\d))表示“当前位置后面必须是一组组三位数字,且之后不能再跟数字”。只看一遍很难懂,但拆开就清楚了:它是在找所有“应该插入逗号的位置”。这就是零宽断言的威力,位置检查配合捕获组和全局匹配,批量处理起来非常方便。
前瞻在 JS 里一直支持得很好,后顾则是 ES2018 才加入的。现在主流浏览器都支持了,但如果你要兼容很老的环境,还是得避开后顾。密码强度校验也是前端面试常客:要求同时包含大写、小写、数字和特殊字符,长度 8 位以上,用前瞻写出来很优雅,但要注意多个断言之间是独立的,它们都在检查同一个位置,所以顺序无关紧要。
3.3 常用正则套路与边界细节
这一节我整理了实际开发中反复出现的几个套路,掌握了能省不少事。
第一个是边界符号的运用。^和$分别表示字符串开头和结尾,做“完全匹配”校验时一定要带上,比如表单校验手机号写/^\d{11}$/,少了^和$,就会误放行abc13800138000def这样的字符串,因为正则默认只做部分匹配。很多人校验不严,bug 源头就在这。
第二个是\b单词边界。\b匹配的是“单词字符\w和非单词字符之间的位置”。比如想匹配独立的 cat,而不想匹配 category 里的 cat,就写/\bcat\b/。注意\b对中文不友好,中文和中文之间不会形成单词边界,所以处理中文文本时经常要绕开它。
第三个是特殊字符的转义。需要匹配.、*、?、+、^、$、[、]、{、}、(、)、|、\这些符号时,都要在前面加反斜杠。比如匹配文件后缀/\.js$/,这里的点如果不转义,就会匹配任意字符,/a.js/也会匹配aXjs之类的内容。转义是最容易忽略的细节,尤其在拼接动态正则时,一定要做统一转义。
这三个套路看着简单,实际项目里能避免一大半低级 bug。
4. 实战案例:从表单校验到数据清洗
4.1 手机号校验:边界条件与最新号段
前端对手机号的校验,核心诉求是“格式是否正确”。目前国内的手机号是 11 位,以 1 开头,第二位通常是 3、4、5、6、7、8、9,后面跟 9 位任意数字。一个够用的正则可以写:
/^1[3-9]\d{9}$/有人会纠结要不要把每个号段都列全。我的建议是:前端做到这个精度就足够了,号段更新很快,把校验写得太死反而会误伤新号段用户。如果业务上实在需要细分,后端可以再做更精确的校验,前端没必要把规则锁死。
实现时还有两个细节。一是使用 HTML 的 input 属性做初步限制,比如maxlength="11"、type="tel",配合 input 事件过滤非数字字符,体验会好很多。二是校验时机:不要每次按键都弹错误,比较友好的做法是输入框失焦时校验,或者点击提交时统一校验。实时校验不是不行,但要注意给用户足够的缓冲。
4.2 邮箱校验:实用版与严谨版的取舍
邮箱正则堪称前端“最容易被抄错”的案例。网上流传的所谓完美邮箱正则又长又复杂,实际用起来还可能把合法邮箱拒之门外。我在生产环境里常用的实用版是:
/^[\w.+-]+@[\w-]+(\.[\w-]+)+$/这个版本能处理name@example.com、name+tag@example.com、a.b_c@example.co.uk之类常见格式,基本够用。注意[\w.+-]里把点、加号、横杠都纳入了用户名常见字符,@后面是域名部分,用(\.[\w-]+)+保证至少有一个点号后缀。
严谨到什么程度才算好?我的建议是按业务风险来。如果是注册流程的关键步骤,宁可放宽一点,再把激活邮件发出去让用户点链接确认,这比前端正则拦死一个合法地址好得多。邮箱格式本身很复杂,域名可以是有特殊字符的国际化域名,正则想要 100% 正确几乎不可能,所以“够用 + 后端兜底”才是工程上的合理选择。
4.3 车牌号校验:普通燃油车与新能源车
车牌号校验在停车场管理、车辆登记类页面很常见。普通车牌的结构是:省份简称(1 位汉字)+ 发牌机关代号(1 位字母)+ 序号(5 位,字母和数字混合,但一般不能有 I 和 O)。新能源车牌是 8 位,比普通车牌多一位,是汉字 + 字母 + 6 位序号,且最后一位必须是字母(D 或 F 加一位数字的情况也有,实际规则更细,不同地区有差异)。
一个业务上常用的校验正则如下:
// 普通燃油车 /^[\u4e00-\u9fa5][A-Z][A-HJ-NP-Z0-9]{5}$/ // 新能源车 /^[\u4e00-\u9fa5][A-Z][A-HJ-NP-Z0-9]{6}$/这里的[\u4e00-\u9fa5]是常用中文字符范围,[A-HJ-NP-Z]是排除了 I 和 O 的字母集合,因为这两个字母和数字 1、0 太容易混淆。实际做页面时,我一般还允许用户输入的字母统一转大写再校验,避免大小写混着比对出问题。
上面提到的中文字符范围,我建议直接用码点范围而不是个别字库,这样覆盖面更稳。当然,如果业务需要严格按最新的车牌编码规则走,还是以后端或公安接口的校验为准,前端主要解决“看起来像不像”的问题。
4.4 数据清洗实战:提取、脱敏与格式化
正则除了校验,第二大用途是对已有字符串做清洗。
提取所有数字,用/[^0-9]/g去替换或者用/\d+/g去 match。比如从一串包含金额、编号的文本里抽出所有数字:const nums = str.match(/\d+/g)。
手机号脱敏是另一个高频需求。把13800138000变成138****8000,正则一行搞定:
str.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')这里的$1、$2引用的是两个捕获组,也就是前三位和最后四位,中间四位被星号替代。注意要确保输入恰好是 11 位,否则这个正则不会匹配,也就不会脱敏。
千分位格式化也可以顺手做掉:str.replace(/\B(?=(\d{3})+(?!\d))/g, ',')。这个正则我在上一节拆过,这里直接拿来用。使用前最好先确认字符串是纯数字或者数字部分在前,否则小数点和负号的位置会被影响。
清洗类正则的通用建议:先明确边界,再动手写。比如“提取所有数字”,数字之间如果夹杂中文、逗号、小数点,结果是否符合预期,先想清楚再操作,免得清洗完还要人工返工。
4.5 身份证校验:长度与格式之外的问题
热词里总有人问身份证号码怎么用正则校验,这里补充一下。大陆身份证号码是 18 位,前 6 位地区码,中间 8 位出生日期,最后 4 位包含顺序码和校验码,校验码可能有X。一个常用的格式校验正则:
/^\d{6}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/这个正则保证了:地区是 6 位数字、年份前缀是 18/19/20、月份在 01-12、日期在 01-31、最后三位数字加一位数字或 X。但它无法校验 2 月是否有 30 号,也无法校验最后一位校验码是否真的正确。真正严格的做法是单独解析出生日期、调用校验码算法,这两个步骤一般放在后端或专门的工具函数里。
前端拿这个正则做第一道过滤足够了,能把明显错误的格式挡在门外。如果想更进一步,可以在前端把第 17 位乘以对应权重、累加后取模算出校验码,和最后一位比对,这套算法网上有现成实现,复制前记得跑通测试用例。
5. 常见问题与排查技巧实录
5.1 匹配结果和预期不符:先检查这几处
正则写出来不生效,我在社区答疑时遇到最多的是下面几种情况。
一是没带头尾锚点。前面说过,/138/能匹配13800138000,也可能匹配其他含 138 的字符串,如果你想做完整匹配,^和$不能省。
二是match带不带g。同一个正则,带g返回纯匹配列表,不带g返回第一个匹配加捕获组,结构完全不同。发现结果里出现index、input这样的字段,说明没带g;发现多出来一堆捕获组的值,可能是分组太多。
三是字符串里的反斜杠问题。new RegExp('\\d')和new RegExp('\d')是完全不同的,后者在字符串里\d会被解释成d。写动态正则时,先console.log一下你拼接出来的字符串,确认反斜杠是否正确,能省很多调试时间。
四是大小写问题。i修饰符没加,导致用户输入House时匹配不到house。这种问题定位最快,先看修饰符再看模式。
提示:遇到匹配结果不对,先别急着改正则,把当前正则、目标字符串和实际输出都打印出来,肉眼对比一遍,90% 的问题能当场发现。
5.2 灾难性回溯:正则把页面搞卡死
正则导致页面卡死并不是玩笑。某些正则结构在匹配失败时会导致灾难性回溯,指数级消耗 CPU 时间。经典罪魁祸首是嵌套量词,比如/(a+)+b/匹配一串全是 a 却没有 b 的字符串时,引擎会不断尝试各种分组方式,直到超时。
业务代码里,解析复杂文本时我见过太多类似的写法,比如(.*)*、(?:[^,]+,)*之类的嵌套。排查方法优先看模式里有没有“量词套量词”的结构。如果确实需要匹配复杂的嵌套结构,比如 HTML,建议不要硬用正则,改用专门解析器;需要匹配宽松内容时,用非贪婪模式 + 明确字符类,比如[^\n]*,能大幅降低回溯风险。
JavaScript 目前还没有原子组,所以没法用(?>)直接切断回溯。一个替代思路是用前瞻模拟:把嵌套部分改写成确定性的结构,或者对输入长度先做检查。这个属于进阶话题,但前端性能排查遇到正则卡顿,至少要知道方向。
5.3 中文和 Unicode:\w 认不出汉字
JS 默认的\w、\d都是基于 ASCII 的,\w不匹配汉字。很多新手写正则匹配中文昵称时,写/^\w+$/校验,结果中文全部不通过,就是这个原因。
处理中文最核心的工具是 Unicode 码点范围。比如匹配中文字符可以用[\u4e00-\u9fa5],匹配中英文数字混搭可以用/^[\u4e00-\u9fa5A-Za-z0-9]+$/。加了u修饰符后,还可以用更精确的 Unicode 属性转义:/\p{Script=Han}+/u,不过这种写法对浏览器的兼容性要求更高,生产环境要看着点使用。
emoji 也是同样的道理。一个 emoji 可能占 2 个码点,没有u修饰符时,.和[\s\S]会把 emoji 劈成两半。处理包含 emoji 的用户内容时,尽量带上u修饰符,用/\p{Emoji}/u之类来判断,性能上也会有帮助。
5.4 调试工具与可读性习惯
最后分享几个我日常用得很顺的技巧。
第一,线上正则调试工具一定用起来。regex101.com 这类工具能实时高亮匹配位置、解释每个符号的含义、显示捕获组和步骤消耗,排查复杂正则效率很高。在工具里确认没问题,再复制回代码里。
第二,正则也讲究可读性。用命名捕获组替代一堆$1、$2;用(?:)去掉无关捕获组;同一个正则拆成几个小的中间变量,分别起好名字。比如先判断手机号格式,再判断号段,最后判断运营商,每一步的意图都清楚,后续维护的人会感谢你。
第三,复杂正则一定要写注释。可以把模式拆成多段说明,把“为什么要这么写”留下来。代码评审时看到一堆乱码一样的正则,真的很劝退,稍微加两句注释,观感完全不同。
我个人是“少背多看、多写多测”的拥趸。正则符号虽然多,但常用组合真的就那几十个。遇到新场景先想清楚输入和输出,再去工具里写一点点验证,比硬背全语法效率高得多。这几年做前端,正则帮我处理过配置解析、日志清洗、表单校验、路由匹配……几乎每一次用回都值得。这篇把 JavaScript 正则的基础框架、API 用法和实战坑点都过了一遍,你把它当成自己的学习手册,常翻常新就好。