拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JavaScript正则表达式实战:从语法到性能避坑指南

写前端这些年我发现一个挺反直觉的现象很多 JavaScript 语法用得很溜的开发者一碰到正则表达式就习惯性地打开搜索引擎现搜现抄。倒不是因为正则真的有多难而是它和普通编程语言长得完全不一样像一套需要单独学的“小语言”。可恰恰是这套小语言在字符串处理这件事上几乎是 JavaScript 里最锋利的一把刀。无论是表单校验、接口数据清洗、日志提取还是前端路由匹配正则只要用对了十行代码能缩成一行还跑得更快。这篇文章不想写成 MDN 文档的翻译版而是想借着我这些年实际项目里反复用、反复踩坑的经验把 JavaScript 正则在真实开发里该怎么理解、怎么写、怎么调、怎么避坑一次性讲透。适合已经把 JavaScript 基础语法过了一遍、想真正掌握字符串处理能力的开发者也适合写了两三年业务代码但对正则始终“会抄不会写”的朋友。1. 正则表达式到底在解决什么问题很多人第一次接触正则是在表单校验的场景判断一个手机号是不是 11 位数字、一个邮箱格式对不对。这个认知没错但如果把正则只当成“校验工具”就太小看它了。1.1 字符串处理的痛点为什么你会用上正则我先说一个日常场景。假设你从后端接口拿到一段文本需要把里面所有的数字提取出来没有正则之前你会怎么写大概率是循环字符串用charCodeAt或者parseInt逐字符判断再拼到一起。这段逻辑不算难但写起来少说要七八行而且每次遇到“提取”“替换”“截断”这类需求都要重新捣鼓一遍循环。正则的定位恰恰就是解决这一类问题。它本质上是一种声明式的字符串匹配语言——你不告诉程序“怎么一步一步找”而是告诉它“你想要的模式长什么样”。JavaScript 引擎拿到这个模式会自己完成查找、比对、回溯这一整套流程。所以很多时候正则表达式不是在替代你的代码而是在替代你手写的循环和判断。当然正则也不是没有代价。它读起来确实像天书写错了还不会立刻报错而是非预期匹配。这也是为什么很多人对它又爱又恨。但换个角度想任何一门语言都要花时间学语法正则也是一样只是它的语法符号比较浓缩罢了。1.2 执行引擎的工作方式匹配一个字符串到底发生了什么理解正则的匹配过程比背 30 个元字符更有用。JavaScript 的正则引擎属于回溯型 NFA非确定性有限自动机。什么意思呢它从左到右扫描文本每到一个位置会尝试把正则的各个分支和文本去匹配。如果某个分支走不通它会“退回去”换另一个分支再试。我打一个比方。你走进一条商场走廊看到左右各有一扇门每扇门后又有岔路。你的目标是找到一扇贴着“目标”标签的门。NFA 的做法是先推左门一直往里走走不通就退回起点再推右门。这个“退回”的动作就是回溯。大多数时候回溯没什么感觉但一旦正则写得特别复杂文本又特别长回溯次数会指数级上升这就是后面要说的“灾难性回溯”的根源。所以你在写正则的时候脑子里要有一根弦这个模式在极端输入下会让引擎尝试多少次这不是理论问题而是直接影响页面卡不卡的现实问题。1.3 在 JavaScript 中使用正则的两种方式JavaScript 里创建正则有两种方式。第一种是字面量写法也是最常见的const phoneReg /^1[3-9]\d{9}$/;斜杠之间的内容就是匹配模式斜杠后面可以跟修饰符比如i、g。这种写法简洁、性能也好因为引擎在脚本加载阶段就会把正则编译好。日常开发里能用字面量就尽量用字面量。第二种是RegExp构造函数const pattern 1[3-9]\\d{9}; const phoneReg new RegExp(pattern, );构造函数适合什么场景动态生成正则的时候。比如用户输入一个关键字你想在所有商品名里搜出包含它的记录就必须把用户输入的字符串拼到模式里。但这时候有个巨大的坑字符串里的反斜杠需要转义。你写\d在字符串里得写成\\d不然会被当成一个普通的d。这个细节我见过无数人踩过一调试才发现是转义丢了。2. 语法细节拆解从简单到复杂正则语法看着无序其实可以分成几个层次字符怎么表示、数量怎么控制、怎么分组、怎么定位边界。一个一个层次吃透复杂正则也就没那么可怕了。2.1 字面字符与字符类匹配的范围怎么圈定正则里最基础的是字面字符比如/abc/就是精确匹配abc这三个字符。但实际业务中你往往不知道某个位置具体是什么字符只知道它“可能是哪一类”。这时候就需要字符类和转义字符。\d匹配任意一个数字\w匹配字母、数字、下划线\s匹配空格、制表符、换行等空白字符。对应的大写形式是取反\D匹配非数字\W匹配非单词字符\S匹配非空白。这里有个特别容易被中文业务坑到的地方JavaScript 里的\w默认不匹配中文。也就是说/\w/去匹配你好abc结果只会拿到abc。如果你要匹配“任意中文字符或单词字符”不能只写[\w\u4e00-\u9fa5]得显式把中文的 Unicode 范围加进字符组里。字符组用方括号表示比如[abc]表示匹配 a、b、c 中的任意一个[0-9]表示匹配 0 到 9 的任意数字[^a-z]表示匹配任意非小写字母的字符。注意^在字符组里面是否定符在字符组外面是行首锚点两个含义别搞混。2.2 量词、分组与引用一个字符出现多少次匹配到“一个数字”还不够你经常需要匹配“3 个数字”或者“至少一个字母”。这就是量词的活。*表示前面的字符出现 0 次或多次表示至少出现 1 次?表示 0 次或 1 次。更精确的还有{n}正好 n 次、{n,}至少 n 次、{n,m}n 到 m 次。说到量词必须讲清楚贪婪和懒惰这两个概念。默认情况下量词都是贪婪的也就是“能多匹配就多匹配”。比如字符串是a123b456b正则/a.*b/会匹配到从第一个a到最后一个b的整个串也就是a123b456b。但如果你只想匹配到第一个b就要用懒惰量词.*?写成/a.*?b/结果就是a123b。分组用圆括号它有两个作用一是把多个字符当成一个整体应用量词比如/(ab)/匹配一个或多个连续的ab二是捕获匹配的内容后续可以通过$1、$2反向引用。如果你只是想分组、不想捕获可以写成(?:ab)这种叫非捕获分组性能会好一丢丢在 replace 操作时也不会干扰编号。2.3 断言与边界不会吃掉字符的匹配断言是正则里最“高级”的玩法也是新手最懵的部分。^和$分别是行首和行尾锚点\b匹配单词边界比如/\bcat\b/能匹配a cat里的cat但不会匹配category里的cat因为前后没有单词边界。更复杂的断言叫“零宽断言”意思是它只判断“这个位置是否符合条件”但不会把字符消费掉。常见的四类(?...)正向前瞻右边必须匹配但不消耗字符(?!...)负向前瞻右边必须不匹配(?...)正向后瞻左边必须匹配(?!...)负向后瞻左边必须不匹配举个例子你要把数字金额的千分位加逗号核心思路是在“从右往左数的每组三位数字前面”插入逗号。用前瞻可以写/\B(?(\d{3})(?!\d))/g。这个模式里\B保证不在开头插入(?...)往前看是否整数部分是 3 的倍数且后面不是数字。这种场景如果不靠断言手写循环会很麻烦。后瞻断言在不同浏览器里的兼容性曾经是个问题不过现在主流浏览器都支持了。生产环境还是要看一眼目标用户的浏览器版本再决定。2.4 常用标志位与 Unicode 处理修饰符虽然写在正则末尾但作用很大。g是全局匹配影响match、exec的行为i是忽略大小写m是多行模式让^和$可以匹配每行的开头和结尾而不仅仅是整个字符串的开头结尾s是让.可以匹配换行符这个标志在 ES2018 才引入非常实用u是 Unicode 模式配合\p{...}可以按 Unicode 属性匹配。举个例子你想匹配任意一个中文字符在支持u和属性转义的环境里可以直接写/\p{ScriptHan}/u这比列出一大段 Unicode 范围靠谱得多。但要注意属性转义对运行环境有要求老项目里用之前先确认一下兼容性。3. 实际业务场景中的正则应用掌握了语法下一步就是实战。正则的价值从来不是“能写出来”而是“在合适的地方用对”。这一节我把日常开发里最高频的场景和对应写法整理出来基本都是可以直接改改就用的。3.1 除了校验正则还能做什么校验只是正则的起点。实际开发里正则更大的价值体现在提取、替换和分割这三个动作上。提取从一段杂乱文本里把关键信息捞出来比如从日志中提取时间戳、从用户输入里提取所有 URL。替换做数据脱敏、消息模板渲染、富文本清洗。比如把用户手机号中间四位替换成星号一行replace就能搞定。分割按某种模式把字符串拆成数组比如按连续空白符切割句子。这三个动作在 JavaScript 里对应的就是match、replace、split三个方法。很多开发者只会在test方法里用正则其实是浪费了这门小语言的一大半能力。3.2 六个常见需求模式与代码示例我先说手机号。国内手机号目前是 1 开头第二位是 3 到 9 之间的数字后面是 9 位数字。用正则表达就是/^1[3-9]\d{9}$/。这里^和$必须加不然1234567890123这种 13 位的串也会匹配成功因为正则只要求“包含”而不要求“全串匹配”。邮箱稍微复杂一点因为允许点号、加号、下划线等字符。一个项目中够用的版本是const emailReg /^[\w.-][a-zA-Z0-9-](?:\.[a-zA-Z0-9-])$/;前面的部分允许字母数字下划线点号加号减号后面的域名部分允许字母数字和连字符最后是一个或多个“点域名后缀”的组。注意这里用了非捕获分组(?:...)因为我们在替换时不需要引用这一整段。URL 的匹配在生产环境里其实挺麻烦因为合法的 URL 形式太多了。简版可以做const urlReg /^https?:\/\/[\w.-](?::\d)?(?:\/[^\s]*)?$/i;这个能匹配带协议、可选端口、可选路径的 HTTP/HTTPS 地址应付大多数前端校验够了。真要支持各种奇奇怪怪的国际域名建议去查 WHATWG URL 标准的解析器实现而不是硬写一个千万级的正则。密码强度的典型要求是“8 到 20 位至少包含大写字母、小写字母、数字中的两种”。这种需求连续断言最合适const pwdReg /^(?.*[a-z])(?.*[A-Z])(?.*\d)[a-zA-Z\d]{8,20}$/;三个(?...)分别保证必须存在小写、大写、数字最后一部分控制整体字符集和长度。注意字符集要写[a-zA-Z\d]这样特殊符号直接不允许也就自然满足“只允许三类字符”的要求了。提取所有数字可以用match加gconst nums 订单号2024A-003金额89.5元.match(/\d(?:\.\d)?/g); // 结果 [2024, 003, 89.5]替换成驼峰命名的经典案例是把 CSS 属性转成 JS 里的驼峰写法const camel background-color.replace(/-(\w)/g, (_, c) c.toUpperCase()); // 结果 backgroundColor这里回调函数里的第一个参数是完整匹配-c第二个参数是捕获组(\w)里的内容返回它的大写形式即可。3.3 与其它语言正则的差异提醒如果你在 Python、Java、C# 里都用过正则会发现在 JavaScript 里有几个需要注意的差异。JavaScript 的正则字面量是一种“对象字面量”它的字面量写法会被解析器当成代码的一部分所以如果你要动态拼接模式只能走RegExp构造器。JavaScript 的正则没有“递归匹配”语法有些复杂嵌套结构比如匹配带嵌套括号的表达式基本无能为力别硬写。命名捕获组的语法/?name.../在 ES2018 之后才支持太老的环境会直接语法报错。所以如果项目还要兼容老旧浏览器写之前先查一下引擎支持情况。replace的第二个参数可以传回调函数返回值会覆盖匹配内容这个能力在 Java 里对应的是Matcher.replaceAll但 JavaScript 里写起来更顺手。4. 常见问题排查与性能陷阱正则这东西写出来能跑是个层次写出 bug 少、性能稳的正则又是更高的层次。我自己经历过太多“正则没问题数据有问题”的诡异时刻后来总结了一套排查思路分享出来给大家少走弯路。4.1 常见问题速查表现象可能原因解决方案校验时好时坏正则用了g标志test或exec受到lastIndex影响去掉g或每次调用前重置lastIndex 0中文匹配不到用了\w但它只匹配 ASCII 字母数字下划线改用[\u4e00-\u9fa5]或\p{ScriptHan}.匹配不到换行点号默认不匹配\n加s标志或改用[\s\S]替换只替换了第一处replace默认只替换第一个匹配加g标志字符串里的\d变成d用RegExp构造时字符串转义丢了反斜杠要写成\\d数字校验允许了字母忘了加^和$变成“包含匹配”加首尾锚点或直接用fullmatch语义这里面我特别想强调第一行。$这个问题我在真实项目里见过好几次一个全局正则对象被多处调用test的结果一会儿 true 一会儿 false排查半天才发现是lastIndex在作怪。g标志会让正则记住上次匹配结束的位置下一次从那个位置继续找一旦字符串不满足lastIndex就归零。这个特性在循环里配合exec提取多个匹配时很好用但在单纯校验时就是隐形炸弹。4.2 灾难性回溯正则也能拖垮页面前面说到 NFA 的回溯机制这里讲一个真实风险——灾难性回溯ReDoS。一个经典的正则是/(a)b/看起来只是匹配一堆a后面跟一个b。但如果输入是aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac一堆a加一个c引擎会怎么做最外层的(a)可以拆成多种组合方式第一个a匹配 5 个剩下交给第二个也可以第一个匹配 3 个……于是引擎疯狂尝试这些组合全部失败后再逐层退回耗时呈指数增长。几十个a就够让页面卡死几秒。解决办法有几个方向。一是简化模式像上面这个例子完全可以写成/ab/不要嵌套量词。二是加锚点定界让匹配失败尽早发生。三是对输入长度做上限正则本身不背这个锅业务层限长是最直接的兜底。我平时写校验正则时会习惯性地问自己一句如果用户传了一个 10 万字符的字符串进来这个正则会怎样如果答案里有“疯狂尝试”四个字我就会重新写。4.3 调试与测试的经验技巧正则写错不可怕可怕的是不知道怎么排查。我自己的调试流程一般是三步。第一步用测试工具拆解。在浏览器开发者工具的控制台里可以直接敲正则表达式和测试字符串看匹配结果。也可以在 Regex101、Regexr 这类在线工具里把表达式粘贴进去它会实时高亮匹配位置还会显示分组捕获的内容、调试引擎的匹配步数。这个“匹配步数”特别有用一眼能看出性能瓶颈在哪。第二步写边界用例。正则的坑往往不在正常数据而在边界数据空字符串、全字符、只有一位的数字、超长文本、包含换行的文本、包含中文的文本。我写业务正则会习惯性列一组测试用例至少五六个全部跑过才敢合并到代码里。第三步在代码里加注释。这个建议看着简单但极其实用。复杂正则过一个月回看连自己都未必记得每个符号的意图。我会在正则上方用两行注释写明“匹配什么场景”“哪个部分对应什么业务含义”以后维护成本能降一个量级。最后分享一点我的个人体会写了这么多年正则我最深的感触是正则不是背出来的是“拆”出来的。遇到一个复杂需求先别急着写一长串而是把它拆成几个小问题第一步匹配什么、第二步判断什么、哪个部分是边界条件。然后一个个小问题分别用最简单的正则实现最后再合并。这样写出来的东西既不容易错也好维护。另外团队项目里我不太建议大家追求“一行炫技式”的正则。比如匹配邮箱那个例子写成那种一行模式没毛病但如果你要处理更复杂的业务规则不如分成两步先做基础格式校验再做业务规则判断每一步单独测试。代码可读性带来的维护收益远远大于“少写三行”带来的快感。正则这门小语言真正难的不是语法而是“匹配思维”——从模式的角度去看字符串。一旦你习惯了这种思维方式很多以前觉得头疼的文本处理问题都会变成一件挺顺手的事。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门