
写表单做校验这么多年数字正则看起来是最不起眼的需求真上手才发现里面全是坑。你以为写个^\d$就完事了用户给你输入个-3.14、.5、1e10、01分分钟教你重新做人。这篇教程我把自己在实际项目中踩过的坑、沉淀下来的写法全整理了一遍包含整数、小数、正负号、指数形式、边界匹配这些场景并且附上了可直接复制使用的封装函数。不管你是刚接触正则的新手还是被表单校验折磨过的老手这篇内容都应该能帮到你。1. 数字正则的整体设计思路1.1 先从需求说起校验、提取与清洗在写正则之前必须先搞清楚一个事情你要的是“校验单个字符串是不是数字”还是“从一段混杂文本里把数字抠出来”还是“把用户输入的数字清洗成标准格式”这三个场景对应的正则写法完全不同而且经常有人混为一谈。校验Validation判断3.14是不是合法数字。此时正则必须使用^和$锚定整个字符串要求从头到尾完全匹配。提取Extraction从价格是12.5元重量3kg中抽出12.5和3。此时正则只负责匹配数字片段不能加^和$还得考虑数字前后可能出现的字符边界。清洗Sanitization把1,234.56中的逗号去掉或者把abc123中的非数字字符移除。这通常结合String.replace()来做正则只是辅助工具。这三种需求混为一谈是新手最容易犯的错。很多人拿着从某个代码片段里抄来的正则发现“为什么我明明匹配到数字了校验还是失败”——原因往往就是锚点位置不对。这个思路问题不解决后面写再多正则都是白搭。1.2 正则匹配数字的基本功元字符与量词正则匹配数字的基本组成单元不多但组合方式千变万化。核心元字符如下元字符含义说明\d匹配任意数字等价于[0-9]注意它不匹配全角数字\d匹配一个或多个数字量词表示“至少一次”\d*匹配零个或多个数字量词*表示“零次或多次”非常容易匹配到空字符串\d{2}匹配恰好两个数字量词{n}表示精确匹配\d{1,3}匹配1到3个数字量词{min,max}表示范围匹配.匹配任意字符换行除外用于匹配小数点时必须转义为\.\.匹配小数点字符转义后的小数点不会误匹配任意字符-匹配连字符在字符组外表示范围在字符组内放末尾表示字面量^匹配字符串开头在字符组内表示取反如[^0-9]$匹配字符串结尾与^配合实现全字匹配理解这些基础之后你会发现数字正则的复杂程度完全取决于你要允许哪些数字形态。只允许正整数允许负号允许小数点允许.5这种省略整数部分的写法允许科学计数法每算一种形态正则复杂度就上一个台阶。这也是为什么我建议先从简单版开始一个需求一个需求地加而不是一上来就抄一个巨长的“万能正则”。2. 整数匹配的多种写法与关键边界2.1 最简单却最容易出问题的版本先看最常见的需求校验一个字符串是否为非负整数。很多人第一反应是const regex /^\d$/;实测一下regex.test(123); // true regex.test(0); // true regex.test(007); // true (注意这里允许了前导零) regex.test(); // false regex.test( 123); // false因为 ^ 匹配字符串开头空格导致失败 regex.test(123 ); // false这个正则看起来简单但在实际业务中往往不满足要求。比如在年龄、数量、编号这类场景里007这种带前导零的输入可能不符合预期。更关键的是/^\d$/匹配的是任意长度的数字串如果你限制了字段最大位数比如身份证后几位、验证码那就需要精确控制长度// 匹配 6 位数字 const sixDigit /^\d{6}$/; // 匹配 4~8 位数字 const rangeDigit /^\d{4,8}$/;但这里有个隐藏的坑\d在 JavaScript 中默认是 ASCII 数字0-9。如果你用了某些 Unicode 标志或者字符串中包含全角数字如\d不会匹配它。对于绝大多数国内业务场景全角输入其实经常出现表单校验时建议明确使用[0-9]替代\d避免用户的输入法搞出你想象不到的字符。2.2 考虑正负号与“正整数/负整数/非负整数”的分类业务里很快会遇到带符号的场景温度、海拔、余额变动、坐标数据。这时要区分几个不同的概念正整数1, 2, 3...不含0负整数-1, -2, -3...非负整数0, 1, 2, 3...非正整数0, -1, -2, -3...任意整数包含 0、正数、负数写法分别如下// 正整数不含0 const positiveInt /^[1-9]\d*$/; // 负整数不含0 const negativeInt /^-[1-9]\d*$/; // 非负整数含0 const nonNegativeInt /^\d$/; // 非正整数含0 const nonPositiveInt /^(0|-[1-9]\d*)$/; // 任意整数 const anyInt /^-?\d$/;仔细对比一下任意整数和正整数两种写法。/^-?\d$/用了可选负号-?它允许-0这种形式因为-\d会和0匹配。在某些校验场景下-0可以被接受但在科学计算、坐标显示这类场景就不太行。如果你要完全禁止-0就得用分支组合const anyIntNoNegativeZero /^(0|-[1-9]\d*|[1-9]\d*)$/;这个正则的思路上来了用|把零、负整数、正整数三个分支枚举出来。这种枚举法在正则里其实很常用——当你要穷举“合法形态”时加分支往往比试图用一个通式去匹配更可靠。2.3 区间限制匹配 1 到 100 的整数比单纯正负号更麻烦的需求来了不仅要整数还要范围限制比如“年龄在 1~100 之间”。很多人会想^[1-9][0-9]?$|^100$之类。没错正则做区间匹配的本质是把区间拆成若干个子区间然后按位匹配。这里有个规律区间范围越小越好写越大越痛苦。1~99 可以写成^[1-9][0-9]?$但 1~100 就得单独把 100 加进去const ageRegex /^(100|[1-9][0-9]?)$/;如果是 1~1000就得拆成1~9、10~99、100~999、1000。表达式就变成const range1to1000 /^(1000|[1-9][0-9]{0,2})$/;很多人在这种时候会陷入误区强行用正则做区间判断。然后发现自己写出了几百个字符的天书过两周回来看自己都看不懂。我的建议是如果区间跨度很大或者边界规则复杂比如“16到65岁且不能是18的倍数”别用正则先在正则里宽松匹配数字格式再用数值比较做范围判断function validateAge(input) { if (!/^\d{1,3}$/.test(input)) { return false; } const num parseInt(input, 10); return num 1 num 100; }这种“正则负责形状、数值逻辑负责范围”的混合模式是我在实际项目里用得最多的方式。它的可读性远比一个复杂正则高得多而且调试起来非常直观。3. 小数匹配精度控制与格式陷阱3.1 基础小数正则的演进小数的匹配比整数多一个维度小数点。一个最基础的小数正则长这样/^\d\.\d$/;它可以匹配3.14、0.5、123.456但拒绝3.和.5。对于大多数表单场景这个保守版本完全够用。可一旦你把需求放开一点比如允许3.或.5就得重新设计。常见的小数形态其实有三种形态示例对应需求整数部分小数点小数部分3.14标准写法只要求有小数点和小数部分.5允许省略整数部分只要求有整数部分和小数点3.允许省略小数部分少见如果你要同时允许3.14和.5正则可以写成/^\d*\.\d$/;注意这里把整数部分从\d改成了\d*。但这样会有一个问题它也能匹配.5也可以但它同时会匹配xxx的空整数部分。也就是说/^\d*\.\d$/允许字符串以小数点开头这与很多表单域的设计不符。所以实际项目里更推荐的组合是/^(\d\.\d|\.\d)$/;两个分支清晰地枚举了“有整数部分”和“无整数部分”两种情况。可读性比\d*好得多而且不容易误匹配。但这里还有一个经典的坑上述所有正则都允许00.5、000.1这种带前导零的小数。有些校验场景要求一个数字的整数部分不能以0开头除非本身就是0比如金额校验通常不允许007.00。这时就得上更严格的分支/^(0|[1-9]\d*)\.\d$/;这个正则要求整数部分要么是单个0要么是以1-9开头的一串数字。它匹配0.5、12.34但拒绝00.5、007.00。在应用到金额、计量单位这类场合时这个细节往往是评审时被专门问到的点。3.2 控制小数位数固定精度与范围精度另一个常见需求是限制小数位数。最多两位小数价格、金额常见^\d(\.\d{1,2})?$恰好两位小数^\d(\.\d{2})?$最多三位小数^\d(\.\d{1,3})?$仔细看第一个它用了(\.\d{1,2})?将小数部分整体变成“可选”。也就是说12合法12.3合法12.34合法但12.345会被拒绝。这种写法比^\d\.\d{1,2}$更友好因为它同时允许纯整数输入。这里有一点值得展开为什么业务上常常允许“整数也算小数校验通过”?因为在输入银行卡金额、打分、重量这类字段时用户可能只输入整数你总不能在用户输100的时候提示“请输入小数”。所以(\.\d{1,2})?这个可选组的设计非常实用。但如果你反过来要求“必须有小数”——比如记账系统里区分整数金额和小数金额、或者是税率百分比要求3.5%这种格式就把可选组去掉/^\d(\.\d{1,2})$/;3.3 小数匹配中容易被忽略的精度问题如果只是做输入校验正则已经足够。但如果你要拿着这个正则做后续数值运算就还得考虑 JavaScript 浮点数的经典问题0.1 0.2并不等于0.3。正则本身不会帮你解决精度问题。我做过一个运费计算模块从表单里拿到0.1和0.2转成数字相加的结果是0.30000000000000004显示出来非常尴尬。后来我习惯在拿到数字字符串后用如下方式修正function toSafeNumber(str) { const num parseFloat(str); return Math.round(num * 100) / 100; }如果涉及金额更稳妥的做法是全程用字符串或整数分做运算只在展示时转成小数。这一步是正则之外的必备功课。还有一个容易踩的坑0.1和.1在外观上不同但数值完全相等。如果校验规则允许.5但你后续用parseFloat(.5)解析没问题可如果直接把字符串当成数字传给后端后端的解析逻辑可能不接受以点开头的数字。所以跨端对接时表单最好先做标准化把它补成0.5function normalizeDecimal(input) { if (input.startsWith(.)) { return 0 input; } return input; }这种细节不写进需求文档里但实际联调时几乎一定会遇到。4. 正负号、科学计数法与数字的“终极版”4.1 带符号的小数匹配上面说的小数正则都没有处理正负号。把正负号加上正则就变成/^[-]?(\d\.\d|\.\d)$/;这个写法可以匹配3.14、-0.5、.8、-1.0。但依旧不支持3.这种形式。如果你业务里确实需要“整数部分小数点”这个形态同时存在就要再加分支/^[-]?(\d(\.\d*)?|\.\d)$/;注意这里(\d(\.\d*)?|\.\d)的分组结构第一个分支\d(\.\d*)?处理3、3.、3.14第二个分支\.\d处理.14。不过这个正则会放行3.这种情况验证3.是否符合你的业务规则很重要。多数情况下我建议不放行这种形态因为它容易让用户在输入框里把小数点后面还没输入完就提交了。4.2 科学计数法指数形式的匹配做前端久了身份证、手机号这些常规字段都校验过无数遍后你会遇到一个真正有挑战性的匹配科学计数法表示的数字比如1e10、3.14E-5、-2.5E3。科学计数法的本质是数字部分 字母e/E 可选符号 整数指数。于是正则可以这样写/^[-]?(\d\.?\d*|\.\d)([eE][-]?\d)?$/;逐个拆解[-]?整体符号位可选(\d\.?\d*|\.\d)数字部分。\d\.?\d*匹配12、12.、12.34\.\d匹配.5这类开头小数的形态([eE][-]?\d)?指数部分整体可选。其中[eE]匹配合法的大小写科学计数法标志实测const sciRegex /^[-]?(\d\.?\d*|\.\d)([eE][-]?\d)?$/; sciRegex.test(3.14); // true sciRegex.test(-2.5e-3); // true sciRegex.test(1E10); // true sciRegex.test(.5e2); // true sciRegex.test(abc); // false这个正则几乎覆盖了标准数字的所有形态。但这里要泼一盆冷水除非你的业务非常明确需要处理科学计数法否则我并不建议在普通表单里用这个超集因为1e10这种字符串很容易被用户误触输入。更安全的做法是UI 层限制输入表单里用简单正则做格式校验真正处理科学计数法时交给Number(str)或者判断isFinite()做兜底。4.3 正则不是万能的与 Number.isFinite 的方案对比不得不再提一个很多开发者都踩过的坑用正则判断“是否为数值”时正则的能力边界是有限的。比如你想校验一个字符串是不是合法数字而且允许指数、十六进制、正负号甚至允许InfinityNumber.isFinite(Number(str));这个方案只用一行代码就搞定了。但它的代价是不能独立控制格式细节——比如0x1F会被判定为合法数字31 123 也会通过。如果业务上要求严格格式Number.isFinite就无能为力。所以我的经验法则是如果只是判断“能不能转成数字” → 用Number.isFinite(Number(str))别用正则如果要求格式“必须是十进制数字、不能有空格、不能是十六进制” → 用自定义正则如果既要格式又要范围 → 正则先做格式校验再做数值比较有个反例让我印象特别深某后台管理系统的搜索框要求用户输入“舆情数量大于 X”的过滤条件X 只能是正整数。我用^\d$做了校验结果用户输入了999999999999999999999parseInt后精度丢失过滤结果完全不对。后来我加了位数上限校验最多15位才解决问题。这种“格式校验通过但数值精度炸了”的问题单靠正则很难提前察觉一定要想到数值类型本身的边界。5. 实操封装一个通用数字校验函数5.1 将正则封装为可配置的函数在实际项目里每个表单项的数字需求都不太一样今天整数、明天保留两位小数、后天允许负数。与其每个地方分散写正则不如封装一个公共函数用参数控制数字形态内部统一处理正则和边界逻辑。下面这个封装我一直在自己的工具库里使用整理后分享出来/** * 校验字符串是否为合法数字 * param {string} input 输入的字符串 * param {object} options 配置项 * param {boolean} options.allowNegative 是否允许负数默认 true * param {boolean} options.allowDecimal 是否允许小数默认 true * param {boolean} options.allowScientific 是否允许科学计数法默认 false * param {number} options.maxDecimalPlaces 最大小数位数默认 null不限 * param {boolean} options.allowLeadingZero 是否允许前导零默认 false * returns {boolean} */ function isValidNumber(input, options {}) { const { allowNegative true, allowDecimal true, allowScientific false, maxDecimalPlaces null, allowLeadingZero false, } options; if (typeof input ! string) { return false; } let pattern ; // 符号部分 pattern allowNegative ? [-]? : ; // 整数部分 if (allowLeadingZero) { pattern allowDecimal ? \\d : \\d; } else { pattern allowDecimal ? (0|[1-9]\\d*) : (0|[1-9]\\d*); } // 小数部分 if (allowDecimal) { if (maxDecimalPlaces ! null) { pattern (\\.\\d{1,${maxDecimalPlaces}})?; } else { pattern (\\.\\d)?; } } // 科学计数法部分 if (allowScientific) { pattern ([eE][-]?\\d)?; } const regex new RegExp(^${pattern}$); return regex.test(input); } // 使用示例 console.log(isValidNumber(3.14)); // true console.log(isValidNumber(-3.14)); // true console.log(isValidNumber(03.14, { allowLeadingZero: false })); // false console.log(isValidNumber(3.14159, { maxDecimalPlaces: 2 })); // false console.log(isValidNumber(1e10, { allowScientific: true })); // true这个函数的优势是把决策逻辑收敛到了配置项上而不是散布在十几个正则里。哪天要禁止负数、限制小数位数直接改调用参数不需要重新理解正则。但我必须提醒一下工具函数越通用越要写好测试用例否则换个参数组合就能炸出你没预料到的边界。5.2 完整测试用例速查表为了确保封装不出问题我把常见的输入和期望输出整理成了一张速查表你写完函数后可以直接用来做回归测试输入字符串默认配置允许科学计数法禁止负数最多两位小数0truetruetruetrue3truetruetruetrue3.14truetruetruetrue-3.14truetruefalsetrue3.14truetruefalsetrue.5truetruetruetrue3.truetruetruetrue03.14falsefalsefalsefalse1e10falsetruetruetrue1E-3falsetruetruetrueInfinityfalsefalsefalsefalsefalsefalsefalsefalse其中.5和3.这两个形态需要特别注意上面的封装里\.\d允许了.5\.\d加上\d的备选也能让3.通过。说实话在大多数业务场景中我建议把这两类形态直接禁用掉因为输入框里出现以点开头或以点结尾的合法数字往往意味着用户还没输完。如果要做严格限制可以在函数外面做一次字符串预处理function normalizeNumberInput(str) { return str .trim() .replace(/^\./, 0.) .replace(/\.$/, ); }5.3 正则的动态构造转义与 new RegExp 的坑上面的工具函数使用了new RegExp()动态构造正则这里有一个非常典型、几乎所有前端都踩过的坑字符串反斜杠转义。在正则字面量中\d表示数字但如果你用new RegExp()字符串里的\d必须写成\\d。因为 JavaScript 字符串解析时\d会被转成d这不是你想要的。这也是为什么我看到很多人在动态构造正则时查资料也找不出毛病最后发现少了双反斜杠。再深入一点动态构造还会带来另一个坑用户输入的内容拼进正则时必须做转义。比如你做一个搜索功能让用户输入关键词去匹配数字用户输入了个.在正则里它变成了“任意字符”直接让你的搜索结果异常。通用转义思路是把特殊字符全部处理掉function escapeRegex(str) { return str.replace(/[.*?^${}()|[\]\\]/g, \\$); }任何外部输入只要想当作字面量塞进正则必须先过这层转义。这个习惯能帮你避免掉一大半正则相关的安全问题。6. 常见问题与排查技巧实录6.1 高频问题清单结合我自己以及在团队里帮同事排过的问题整理出一份数字正则的高频报错速查表几乎把“为什么代码看起来没错但就是校验失败”的常见原因都覆盖到了问题现象根本原因解决方案123校验失败但abc123能匹配到正则没加^和$只做了部分匹配加上锚点^和$或者换成.test()并确认整个字符串匹配.test(3.14)返回 false小数点在正则中被当成任意字符匹配了把点写成\.或者放到字符组[.]中允许007通过但业务要求不能有前导零用了\d直接匹配整数部分改为 (0-0被错误接受负号可选、数字部分为\d用 (03.14159误通过“最多两位小数”校验正则写成了\d\.\d*小数位不限改成\d(\.\d{1,2})?用精确量词限制小数位1e5拒绝但Number()能解析正则没有处理科学计数法单独加指数匹配分支或用Number.isFinite()兜底全角数字校验失败\d不匹配全角数字前端输入框限制字符格式或使用[0-9]并做好输入法处理新建RegExp(^\\d$)匹配不到数字字符串里少了双反斜杠写\\d而不是\d6.2 排查正则的实用技巧排查正则问题的时候我很少直接盯着代码“冥想”。更有效的做法是先把正则拆开一点一点做验证。比如在浏览器控制台里new RegExp(^\\d$).test(123)一步就能知道问题出在锚点还是字符集。在日常调试中我习惯准备一个“测试用例列表”把合法输入、非法输入、边界输入各写上几条每改一次正则就跑一遍全量用例。这比靠感觉改代码高效十倍。比如我有一次排查“为什么-5被拒绝”时最后发现是符号位写成了[-]?但是在字符组里-放在了中间被当成了范围连接符。这种细节不是靠肉眼看出来的而是靠测试用例一跑就露馅了。还有一个小技巧值得分享如果正则特别复杂不要试图一口气写完。先把整数的分支写出来跑通测试再把小数部分加进去再跑通最后加正负号和指数。一次只增加一个变量出错了也好定位。正则这东西就怕“写着写着不知道自己写了什么”。6.3 正则的性能与可维护性考量短期来看功能正确是第一位。但放在长期维护的项目里正则的可读性和性能同样重要。性能方面绝大多数数字正则涉及的字符串都很短手机号、金额、年龄所以性能问题几乎可以忽略。但有一种情况需要注意如果你在长文本里反复执行复杂的正则匹配比如把一段几万字的文本里的所有数字提取出来并且用了大量回溯型量词(a)这种嵌套量词极端情况下会造成灾难性回溯导致页面卡死。数字正则一般不会碰到这种极端情况但保持正则的线性结构、避免不必要的嵌套分组依然是良好习惯。可维护性上我的建议是超过 20 个字符的正则一定要写注释。JavaScript 正则本身没有注释语法但你可以把正则拆成数组用join()的方式分段构造然后在每段后面加注释const NUMBER_REGEX new RegExp([ ^, // 开始 [-]?, // 可选符号 (?:, // 数字部分开始 \\d\\.\\d, // 整数小数 |, // 或者 \\d, // 纯整数 ), // 数字部分结束 $, // 结束 ].join());这种写法虽然多写了几行但半年后回来看代码你立刻能看懂每一块在干什么不用再拿着一长串正则去猜。7. 实际项目中的经验沉淀与扩展思路7.1 表单场景下的正则应用策略说回实际业务数字正则绝大多数情况下都是用在表单校验里的。这个场景有几个特点输入框内容短、用户行为不可控、需要即时反馈。基于这些特点我总结了几条策略允许用户在输入过程中处于“临时不合法”的状态。比如用户先输入一个-此时字符串是-如果按最终校验规则判定它会报错。但用户可能正准备继续输入5。所以在监听input事件时不要一上来就用完整正则校验而是先做一个宽松的过滤正则只允许数字、负号、小数点输入在失焦blur或者提交时再做严格校验。输入阶段用清洗代替校验。比如限制只能输入特定字符可以在input事件里直接替换非法字符inputElement.addEventListener(input, (e) { e.target.value e.target.value .replace(/[^\d.\-]/g, ) // 只保留数字、小数点、负号 .replace(/^([^\-]*)\-.*/, $1); // 只允许最前面出现负号 });提交阶段用严格正则兜底。清洗只保证字符集合法但-3.14.15这种字符串清洗后依然存在必须在提交时通过严格正则验证两次防守缺一不可。这个“输入时宽松、提交时严格”的模式是我在无数表单项目里打磨出的最佳实践。7.2 从数字正则延伸出的周边处理写数字正则的过程中你会发现实际上项目里还有很多周边问题需要一起解决千分位格式化把1234567.89显示成1,234,567.89用正则/\B(?(\d{3})(?!\d))/g配合replace实现。它匹配的是“数字边界 每三位数字前面的位置”理解起来有些绕但实用价值很高。提取文本中的数字str.match(/-?\d(\.\d)?/g)可以把一段话里的数字全抓出来这在日志分析、自然语言处理预处理里很常用。金额标准化处理1,234.50、USD 12.5等格式时通常先replace(/[^\d.\-]/g, )去掉干扰字符再用前面的数字正则校验。这些功能看着零散但在实际项目里往往和数字正则同时出现建议你把这几个正则一并收藏进自己的工具库。7.3 最后分享几个我踩过的坑第一次写数字校验时我用的是/^\d$/以为万事大吉。后来线上反馈说有人输入了123abc也能通过校验排查后才发现用户是在英文输入法下误触了字符而我的正则没有锚定结束位置。那次之后我养成了一个习惯校验类正则永远写^和$不做任何妥协。第二次是负数的问题。业务方要求“不能为负数”我写了/^\d(\.\d)?$/以为很安全。结果用户直接粘贴了-5校验通过了。聊过之后才知道原来这个表单是从 Excel 里批量粘贴数据Excel 里的负数自带负号粘贴时负号根本没被过滤掉。从那之后凡是涉及粘贴场景我都会在清洗阶段把非数字字符全处理掉。第三次是精度问题也就是前面说的0.1 0.2那个经典问题。那次之后我学会了正则只管格式数值计算必须单独处理精度。这三个坑让我明白一个道理正则写对了只是第一步把它放进具体的业务场景里还能扛住用户的各种骚操作才算是真正过关。希望这篇教程能帮你把数字正则的“地基”打牢下次再遇到整数、小数校验的时候不用再临时翻文档、抄代码。如果有什么我没覆盖到的特殊数字格式欢迎在评论里提出来我也正好补一补自己的知识盲区。