前端里有一块内容写业务代码的时候绕不开可真要静下心来把它弄明白很多人又拖着一直没做就是标题里这个JavaScript 正则表达式。我见过不少同事一遇到表单校验、字符串截取、批量替换第一反应是去搜“现成的正则”复制过来能用就行一旦规则变了一点就完全不知道从哪儿下手改。这篇文章我打算直接站在 JavaScript 的角度把正则从创建方式、核心语法到实际项目里常用的 API 和场景全部过一遍再把我自己踩过的坑一并说出来。适合刚学前端、准备面试的朋友也适合写了两三年业务但正则一直靠“粘贴-试错-再粘贴”的兄弟。1. 正则表达式到底在干嘛1.1 一句话理解正则的工作方式正则表达式本质上是一种“描述字符模式”的语言。你用一串特殊字符告诉引擎“我想要找这样的文本”引擎拿到这串描述去目标字符串里扫描、匹配再把结果返回给你。生活里可以类比成一个非常较真的快递分拣员你给他一张写满规则的纸条他站在传送带前把每个包裹上贴的面单按纸条上的规则逐字检查符合规则的丢进左边的筐不符合的丢进右边。很多新手容易陷入的误区是想用正则去“解析”复杂的嵌套结构比如 HTML、JSON、带嵌套括号的表达式。正则本身是个有限状态机它擅长的是“按模式匹配”不是“按语法解析”。真正复杂的解析工作应该交给专门的解析器或者拆成多步正则反复处理。理解这个边界能帮你少写很多又长又脆弱的正则。1.2 JavaScript 里怎么创建正则JavaScript 创建正则有两种方式看起来差不多实际差别很大。第一种是字面量写法也是最推荐的方式const phoneReg /^1[3-9]\d{9}$/;第二种是构造函数方式const pattern ^1[3-9]\\d{9}$; const phoneReg new RegExp(pattern, );关键区别在于字面量里写\d引擎直接认识但在构造函数里你传进去的是一个字符串\d写在字符串里会先被字符串转义规则消费掉变成d。所以必须写成\\d才能把反斜杠原样传给正则引擎。这一点是新手最容易踩的坑。那什么时候必须用构造函数答案是当你的正则模式本身是动态拼接出来的比如根据用户选择的条件动态生成校验规则或者从后端接口拿到一段规则字符串。这种情况下字面量写不了只能用new RegExp(...)。除此之外一律优先字面量因为它不用处理字符串转义写出来是什么引擎看到的就是什么少一层心智负担。2. 核心语法按“匹配什么”和“匹配多少次”拆开讲2.1 字符匹配规则与预定义字符类正则里的字符匹配本质就是“这个位置上的字符允不允许是这些字符中的某一种”。最简单的形式是直接写具体字符比如/abc/只能匹配连续的abc。但实际需求很少这么直白所以我们常用“字符组”来限定一批可选字符。写法含义示例[abc]匹配 a、b、c 中任意一个字符/t[ao]p/可匹配 tap、top[^abc]匹配不是 a、b、c 的任意字符/t[^a]p/可匹配 tbp不能匹配 tap[a-z]匹配 a 到 z 的任意小写字母/h[0-9]/可匹配 h1、h8.匹配除换行符外的任意一个字符/h.t/可匹配 hat、hot\d匹配一个数字等价于[0-9]/h\d/可匹配 h1不能匹配 ha\w匹配字母、数字、下划线等价于[A-Za-z0-9_]/h\w/可匹配 h_\s匹配空白字符包括空格、Tab、换行/a\s*b/可匹配 a b\D、\W、\S分别是对应大写形式的取反/\D/匹配任意非数字这一块我建议不要硬背而是记住一个规律小写是“有”某个特征大写是“没有”某个特征。.是个特例它太贪婪但也太模糊很多场景下用[^...]比.更安全这个后面讲 HTML 清洗的时候会再说到。2.2 量词与贪婪、懒惰匹配光能匹配“一个字符”还不够实际需求通常要求匹配“连续若干个”。量词就是干这个的。量词含义*出现 0 次或多次出现 1 次或多次?出现 0 次或 1 次{n}恰好出现 n 次{n,}至少出现 n 次{n,m}出现 n 到 m 次这些量词默认都是“贪婪”的也就是引擎在满足规则的前提下会尽量多吞字符。看这个例子const html span标题/spanspan正文/span; html.match(/./); // 匹配结果是 span标题/spanspan正文/span很多人在这里会困惑我想要的只是第一个span为什么它把整个字符串都匹配了因为.可以匹配任意字符又贪心所以引擎一路吞到最后一个可以满足规则的才停下来。解决办法是让量词变成懒惰模式在它后面加一个?html.match(/.?/); // 匹配结果是 span?表示“尽量少匹配够满足规则就行”。这是正则开发里出现频率非常高的一个坑看到匹配结果比预期长很多第一反应先检查是不是贪婪问题。2.3 位置锚点与边界有些时候你不想匹配字符本身而是想匹配“位置”。比如要求字符串必须从头开始匹配、必须匹配到结尾、只能在单词边界处出现。这些叫锚点。^匹配字符串开头$匹配字符串结尾。这两个在表单校验里几乎必用因为校验通常要求“整个输入必须完全符合规则”而不是“字符串里含有一段合法的内容”。比如/^\d{4}$/.test(12345); // false整体不是4位数字 /^\d{4}$/.test(1234); // true /\d{4}/.test(12345); // true只要求包含4位连续数字注意^和$在 JS 中默认只匹配整个字符串的开头和结尾。如果文本由多行组成并且你希望它们在每一行的开头和结尾生效需要加上m修饰符。\b也是位置锚点表示“单词边界”。它匹配的是单词字符\w和非单词字符之间的位置。看例子/\bcat\b/.test(a cat); // true /\bcat\b/.test(concatenate); // false因为 c 前面不是边界\b对英文单词很好用但对中文无效因为中文没有“单词字符”的概念。所以想匹配中文词边界要么自己定义前后字符集要么干脆不用\b。2.4 分组、引用与修饰符括号在正则里有两个作用一是分组把一段表达式当成整体二是捕获把匹配到的内容存下来供后面引用或替换时使用。const reg /(\d{4})-(\d{2})-(\d{2})/; const result 今天日期是2026-05-20.match(reg); // result[1] 是 2026, result[2] 是 05, result[3] 是 20分组还有一个派生用法叫反向引用在正则内部通过\1、\2引用之前捕获到的那段内容。典型场景是“匹配成对出现的符号”/([]).*?\1/.test(这是一个字符串); // true /([]).*?\1/.test(\这也是\); // true /([]).*?\1/.test(引号不匹配\); // false\1表示“第一个分组捕获到的是什么这里就必须出现同样的内容”这样能保证左右两个引号是同一个。如果你只是想分组不需要捕获用非捕获分组(?:...)。它不产生编号不占用内存代码阅读起来也更清晰。命名捕获组是 ES2018 才支持的但它非常好用const reg /(?year\d{4})-(?month\d{2})/; const result 2026-05.match(reg); result.groups.year; // 2026 result.groups.month; // 05修饰符方面常用的就几个整理成一张表修饰符作用g全局匹配查找所有匹配项而非第一个i忽略大小写m多行模式下^、$匹配每行开头和结尾s让.能匹配换行符u启用 Unicode 模式支持\u{...}和部分转义y粘性搜索从lastIndex位置强制匹配3. JavaScript 正则 APItest、exec、match、replace、split 用熟才算入门3.1 test 和 exec一个是判断题一个是提取题test最常用返回布尔值适合做校验。它内部只关心“有没有匹配上”。/^\d$/.test(123456); // trueexec返回的不是简单的 true/false而是一个数组。数组的第 0 项是完整匹配结果第 1 项开始是各分组捕获的内容同时数组上还挂着index匹配起始位置和input原始字符串两个属性。特别要注意的是当正则带有g标志时exec会“记住”上一次匹配结束的位置存放在正则对象的lastIndex属性里。连续调用exec它会接着上次的位置继续往下找const reg /\d/g; const str age: 18, height: 175; let match; while ((match reg.exec(str)) ! null) { console.log(match[0], reg.lastIndex); } // 输出结果18 7 和 175 22这个特性写字符串解析的时候非常有用可以一截一截地把匹配结果取出来。但要格外小心如果同一个正则对象被两次独立使用第二次的lastIndex可能还停在第一次结束的位置导致结果不符合预期。解决方法是每次使用前手动把lastIndex重置为 0。3.2 match 和 matchAll返回结果大不同String.prototype.match的行为取决于正则有没带g。不带g时它返回的结果和exec基本一致第 0 项是完整匹配后面是分组2026-05-20.match(/(\d{4})-(\d{2})/); // [2026-05, 2026, 05, index: 0, input: 2026-05-20, groups: undefined]带g时它只返回一个数组里面是所有完整匹配的字符串分组信息全部丢失年龄18身高175.match(/\d/g); // [18, 175]如果既要全局匹配又要拿到分组内容推荐用matchAll。它需要正则带g返回一个可迭代对象const reg /(\d{4})-(\d{2})/g; const str 2026-05和2026-06; for (const m of str.matchAll(reg)) { console.log(m[1], m[2]); } // 输出2026 05 和 2026 06日常开发里需要提取多处结构化的数据时matchAll是首选。3.3 replace、search、split日常开发的主力replace是正则里最实用的方法它支持在第二个参数里使用特殊替换模板。比如$表示完整匹配$1表示第一个分组$name表示命名分组。hello world.replace(/\b\w/g, (c) c.toUpperCase()); // Hello World 2026-05-20.replace(/(\d{4})-(\d{2})-(\d{2})/, $2/$3/$1); // 05/20/2026第二个参数也可以是一个函数。函数接收到匹配文本、各个分组、匹配位置、原字符串等参数。这里的自由度就很大了比如把数字替换成中文金额、根据匹配内容动态决定替换结果都是在这个函数里写逻辑。search和indexOf类似区别在于search接收正则返回第一个匹配的位置没有匹配返回 -1订单号A2026-123.search(/\d/); // 4split同样支持正则这在把一段文本按“任意空白”或“多种分隔符”拆开时特别好用 a , b ; c .split(/[\s,;]/); // [, a, b, c]注意结果数组里第一项是个空字符串因为目标字符串开头就是分隔符split会把这个空片段也算进去。想避免这个现象可以先trim()再拆或者用filter(Boolean)过滤空项。4. 实战案例与面试题拆解4.1 手机号、邮箱、身份证号校验手机号校验是校验场景里很典型的一个。国内手机号目前是 11 位以 1 开头第二位现在是 3 到 9后面跟 9 位数字const mobileReg /^1[3-9]\d{9}$/; mobileReg.test(13812345678); // true mobileReg.test(12812345678); // false这个正则好在够宽不把未来可能新增的号段写死又不至于放行明显错误的号码。邮箱校验比较麻烦因为邮箱格式千变万化网上一搜一大把“完美邮箱正则”但很多极端的合法地址反而匹配不了。实际项目里我建议用宽松一点的正则后端再发验证邮件确认比在前端写一个几十行的验证正则更务实/^[^\s][^\s]\.[^\s]$/它只强校验“不能包含空格和 必须有一个点”。这样就挡住了大部分明显乱填的输入又不会误伤合法地址。身份证号校验比前两个复杂。18 位身份证由 17 位数字本体和 1 位校验码组成校验码可能是数字也可能是 X。正则只能负责检查格式真正的有效性要交给后面的加权算法const idReg /^\d{17}[\dXx]$/; function isValidIdCard(id) { if (!idReg.test(id)) return false; const chars id.toUpperCase().split(); const weights [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const checkCodes 10X98765432; const sum chars.slice(0, 17).reduce((acc, c, i) acc Number(c) * weights[i], 0); return checkCodes[sum % 11] chars[17]; }这里的核心思想是正则负责“长得像不像”后续算法负责“是不是真的合法”。两件工具各干各的才是工程上合理的做法。4.2 URL 参数提取与 HTML 清洗从 URL 里取参数是个很常见的需求。用正则可以直接实现const url https://example.com/?namealiceage18citybeijing; const params {}; const reg /[?]([^#])([^#])/g; for (const m of url.matchAll(reg)) { params[decodeURIComponent(m[1])] decodeURIComponent(m[2]); } console.log(params); // { name: alice, age: 18, city: beijing }这里[^#]的意思是“匹配一串既不是 、不是 、也不是 # 的字符”这样参数名和参数值都不会跨到其他分隔符里去。不过老实说浏览器环境里官方提供了URLSearchParams大部分情况直接用new URLSearchParams(window.location.search)更省心。正则方案的价值更多体现在非浏览器环境或者面试中展示你理解字符组的边界意识。清除 HTML 标签也是正则的经典场景。很多人上来就写/\/?[a-z]/g遇到带属性的标签就失灵了。比较稳的写法是const html div classboxpHello/pspanWorld/span/div; const text html.replace(/[^]/g, ); // HelloWorld注意这里用的是[^]而不是.*因为[^]遇到第一个就停不会跨标签吞内容。4.3 千分位格式化与密码强度校验数字千分位格式化被面试官问到的概率不低。纯字符串方法拆来拆去容易出错正则一行就能搞定1234567890.55.replace(/\B(?(\d{3})(?!\d))/g, ,); // 1,234,567,890.55拆开看\B匹配非单词边界因为连续数字内部每个位置都是\B(?(\d{3})(?!\d))表示这个位置后面必须跟着若干个“刚好 3 位的数字组”且这些数字组结束后不能再跟数字了。负向先行断言(?!\d)保证了分组到数字末尾为止不会把 4 位数也当一组。这个写法我自己记了很多年至今觉得它很妙。密码强度校验推荐用“小正则 计数”的写法而不是拼一个巨大的正则这样每条规则清晰、好维护function checkPasswordStrength(pwd) { let level 0; if (/.{8,}/.test(pwd)) level; // 长度 if (/[a-z]/.test(pwd) /[A-Z]/.test(pwd)) level; // 大小写 if (/\d/.test(pwd) /[^A-Za-z0-9]/.test(pwd)) level; // 数字特殊字符 return level; }大正则看着炫但一旦需求说要调整“特殊字符范围”改起来就容易误伤其他规则。5. 正则表达式常见陷阱与调试技巧5.1 三个我踩过的坑第一个坑是lastIndex的残留问题。带g的正则对象如果被复用lastIndex不会自动归零。我在一段循环里用同一个正则验证多行文本第一行匹配成功第二行怎么都匹配失败查了半天才发现是lastIndex卡在一个偏后的位置。从那以后凡是带g的正则只要复用我都在用之前手动置零。第二个坑是贪婪匹配导致结果超预期。有一次我从日志文本里提取[ERROR]和[INFO]中间的内容写的是/\[ERROR\](.*)\[INFO\]/结果.*一路吞到最后一个[INFO]才停下中间跨了好几条日志。后来改成.*?懒惰模式才符合预期。这个经历让我养成了一个习惯写.*的时候先停下来想一想后面这个边界是不是唯一的。第三个坑是灾难性回溯。嵌套的量词比如/(a)$/这种写法在某些特殊输入下会慢到让页面卡死。原因是正则引擎在尝试多种分组可能性时数量是指数级的反复回退尝试所有路径。真实场景里“用一个超大正则匹配长文本”就是风险区。我的对策是能用多个小正分解法的问题不硬凑一个大正则非要写复杂正则先拿超长字符串测一下耗时。5.2 中文匹配、转义与正则调试工具中文匹配是个高频需求。早期前端圈比较通用的是[\u4e00-\u9fa5]/[\u4e00-\u9fa5]/.test(你好世界 hello); // trueES2018 之后在u修饰符下可以用更语义化的写法/\p{ScriptHan}/u.test(你好世界 hello); // true两者都行。\p{...}的写法更接近“自然语言描述”但它要求环境必须支持u修饰符老浏览器会有兼容风险用之前一定确认项目目标。说到转义正则有元字符[、]、(、)、{、}、\、^、$、*、、?、.、|、/这些字符如果只想当普通字符匹配都要在字面量里加反斜杠转义。特别是在字面量形式下/本身还充当定界符所以匹配斜杠时更要转义成\/。调试工具方面我个人习惯是先用可视化工具验证思路再去代码里落地。常用的工具有 regex101、regexr能实时展示分组捕获、替换结果甚至标出每个字符的匹配路径。遇到棘手的表达式我会先在工具里把输入样例和表达式都贴进去确认边界情况再进代码。5.3 常见问题速查表现象可能原因对策匹配结果比预期长很多量词贪婪导致吞过头了把.*改成.*?或改用[^...]同一个g正则第二次匹配失败lastIndex残留复用前手动设置lastIndex 0构造函数里写的\d不起作用字符串转义先消费了反斜杠改成\\d^只匹配了第一段开头未开启多行模式正则末尾加m中文字符匹配失败没有用 Unicode 范围用[\u4e00-\u9fa5]或\p{ScriptHan}match带g时拿不到分组内容match全局模式不返回分组改用matchAll或exec页面卡死、正则执行超时嵌套量词触发灾难性回溯拆分正则、精简嵌套、减少重复分组5.4 我写正则的几个习惯最后说点我自己的实际习惯。第一先写注释再写正则。一行正则写完不注释三天后自己都看不懂更别说同事了。所以我会在正则上方留下一行注释说明它匹配什么、为什么这样写。第二复杂规则拆开写。多个独立条件就写多个小正则用布尔逻辑串起来可读性和可维护性都远胜一个“天书表达式”。第三能不用正则就不用正则。比如startsWith、endsWith、includes、URLSearchParams、String.prototype.trim能解决的简单需求就没必要引入正则。正则是个强大工具但只有在它比普通字符串方法更清晰、更贴合需求时才值得用。把这个基本功练扎实前端面试、日常开发都会轻松不少。下次再遇到需要校验或者提取的场景你至少能自己写出一版能跑的表达式而不是满世界找“现成的”碰运气了。