正则表达式里的[1-9]恐怕是每个新手都会写、但又未必真的理解的一行小玩意儿。很多人一看“匹配1到9”随手就写/[1-9]/结果在10里匹配不到、在123里又只匹配到一个字符一脸懵。其实[1-9]是一个字符集它描述的是“匹配单个字符”而这个“单个”两个字恰恰是大多数人踩坑的起点。这篇就来把[1-9]的语义、用法、组合方式、易错点一次讲透顺带给出前端表单校验、数据清洗、数字提取等真实场景里可以直接抄作业的写法。适合谁看准备学正则的新手写了不少正则但总在数字匹配上翻车的前端以及面试前想系统性过一遍字符集知识的朋友。读完你会明白[1-9]到底能匹配什么、不能匹配什么以及为什么它要和量词、锚点、边界配合着用。1. 基础认知字符集到底是什么1.1 正则里“匹配一个字符”的基本逻辑正则表达式的工作方式是“从左到右、一个位置一个位置地尝试匹配”。一段文本在引擎眼里就是一串字符正则里的每个“匹配单元”都会去对应文本里的一个字符。字符集[...]就是这样一个匹配单元它表示“这一位允许出现方括号里列出的任意一个字符”。拿/[1-9]/来说它表达的意思是在当前位置取文本中的下一个字符如果这个字符的ASCII码落在1到9之间就算匹配成功。一次只匹配一个字符不会自己再去“吞掉”后面的字符。这就是很多人困惑的根源。你心里想的是“匹配数字1到9这个范围”但引擎听到的是“匹配一个字符且这个字符必须在1到9之间”。如果文本是12/[1-9]/匹配的是1而不是12如果文本是0/[1-9]/匹配失败因为0不在1到9这个范围里。注意[1-9]只看字符本身不看“数值大小”。9可以匹配10的1也可以匹配但10作为一个整体没法被[1-9]匹配因为它有两个字符。1.2 [1-9] 的精确含金量为什么是1到9而不是0到9[1-9]中的1-9是“范围简写”等价于[123456789]也就是把1到9这九个字符全部列出来。这个范围包含1和9本身是一个闭区间。它不包含0这是它和[0-9]最本质的区别。有人会问为什么不写成[1,2,3,4,5,6,7,8,9]首先正则字符集里逗号不是分隔符这样写会把逗号也当成可匹配字符其次就算写[123456789]这种展开式日后再调整范围也麻烦。范围写法[1-9]一眼就能看出是从几到几语义更清晰。而且字符集内部还有优化空间引擎对范围形式的处理普遍比一堆展开字符更快。[1-9]的实际语义可以理解为“非零开头的个位数字”也就是1、2、3、4、5、6、7、8、9。这在很多场景下非常关键身份证号第一位不能是0订单号、学号、版本号的第一位往往也不能是0这时候[1-9]就是最直接的选择。1.3 [1-9]、[0-9]、\d 三者到底差在哪这三者经常被混着用但语义并不完全一致。写法匹配范围等价写法典型用途[1-9]1到9不含0[123456789]非零开头的数字、正整数第一位校验[0-9]0到9\d的另一种写法任意一位数字\d0到9[0-9]通用数字匹配书写更简洁\d是[0-9]的简写这一点在JS里没有问题毕竟JavaScript的\d只匹配ASCII数字不会像某些语言那样把全角数字、印度-阿拉伯数字也一并匹配。知道这个底细后当你需要的仅仅是“0到9的任意一位”用\d就行当你明确要“排除0”就必须用[1-9]不能偷懒。[0-9]和[1-9]的差别本质上就是“是否包含0”的一个开关。别小看这个开关做金额校验、编号校验、版本号解析时漏掉0或者多出0结果差得很远。2. 进阶用法量词、锚点和边界怎么搭配2.1 量词跟前缀原理为什么单独写[1-9]只能匹配一个字符光秃秃的/[1-9]/在实际项目中几乎无用。因为一次只匹配一个字符你很难靠它完成“匹配一个多位数”的任务。要让字符集发挥作用必须搭配量词。量词的作用是修饰前一个匹配单元的“出现次数”。三个最常用的量词表示一次或多次*表示零次或多次?表示零次或一次{m,n}表示m到n次。所以/[1-9]/表示“连续出现的一次或多次1到9之间的字符”/[1-9][0-9]*/表示“第一个字符是1到9后续跟着任意个0到9”这就能匹配10、99、123、9999等正整数。*允许后续出现0次所以一个单独的数字也能匹配。如果连量词都不加/[1-9]/在123上只匹配1在其他逻辑不特殊处理的情况下你拿到的只是第一个符合条件的字符。这符合引擎的语法但经常不符合人的直觉。因此一个合格的正则表达式从来都是“字符集量词锚点”三位一体的。2.2 表单校验实战非零开头的正整数怎么写表单里最常见的一个需求校验用户输入一个正整数并且不能以0开头。这个需求拆解下来就是三句话字符串开头第一位必须是1到9后面可以跟一个或多个0到9的数字从开头到结尾不能有多余字符。对应正则就是/^[1-9]\d*$/。这里^表示以这个匹配单元的起点作为整个字符串的起点$表示结束位置。中间的[1-9]管住第一位\d*管住剩余位。同样如果要限制长度比如“1到3位的非零开头数字值范围大致1到999”可以写成/^[1-9]\d{0,2}$/。这里\d{0,2}表示0到2个数字配合第一位的[1-9]最长三位最短一位。需要注意[1-9]\d{0,2}最多只能保证长度不超过3如果业务上要求“范围1-999”这个正则已经够用如果要求“范围1-100”光靠长度约束就不行了还得把正则拆得更细。这种“语法上允许但业务上超限”的问题是正则应用里最常见的隐蔽bug之一。2.3 用g标志配合全局提取把字符串里的个位数字捞出来在实际的字符串处理里我们经常不是做“整段校验”而是做“扫描提取”。比如日志文件里有订单A12入库3件退货B05共2件想把所有1到9的个位数字提取出来就可以用/[\u4e00-\u9fa5]/那种思路的同类写法——注意这里是/[1-9]/g。关键在g标志。没有gmatch方法返回的是第一个匹配结果的数组有g后才会返回所有匹配。看一下这段代码const text A12-B05-C99; const result text.match(/[1-9]/g); console.log(result); // [1,2,5,9,9]0没有出现因为[1-9]本来就不含0。如果你想把05里的5也提取出来就必须另想办法比如把正则改成/[0-9]/g然后单独过滤0这已经超出了单纯一个字符集能解决的范围。把g和test放一起时要格外小心。test方法在有g标志的情况下会从上次匹配结束的位置继续找同一个正则对象反复test结果会来回变化。很多人在这里吃过亏const re /[1-9]/g; console.log(re.test(123)); // true console.log(re.test(123)); // true从索引1继续 console.log(re.test(123)); // false已经到尾了如果只是判断“是否包含”别加g或者每次用一个新正则对象。这类问题排查起来痛苦就是因为结果时灵时不灵。3. 避坑手册这几个常见误区我见过太多次3.1 误区一拿[1-9]去匹配“10”把/[1-9]/拿去匹配10期望得到“匹配成功”结果发现只有1被匹配上0被漏掉。更糟的是用test一测/[1-9]/.test(10)返回true你以为成功了但其实只匹配了第一个字符。如果你的目标确实是“文本里出现过1到9任何一个数字”这样没毛病但如果目标是“匹配整个数字10”那正则应该是对整段文本做整体匹配比如/^10$/或者更进一步允许匹配任意非零开头的数字写成/^[1-9]\d*$/。一个原则是先想清你是要“抓一个字符”还是“抓一整段数字”。前者用字符集后者用字符集加量词。反思自己写正则之前先把目标量化描述出来比直接敲代码靠谱得多。3.2 误区二以为[1-9]能匹配“字符串1到9”有次有人拿着需求“匹配1到9”来问我问他文本长什么样他说文本是“数字1到9”。他是想匹配1到9这三个字符组成的字符串却写成了/[1-9]/当然永远匹配不到。正则里的[1-9]是字符集不是“文本片段”的替代词。如果你想匹配字面意义上的“1到9”这三个字就写/1到9/。这种歧义在需求沟通阶段特别常见。产品说“匹配1到9”你得追问是匹配数字1-9任意一个还是匹配从数字1到数字9之间所有可能的数字还是匹配中文短语“1到9”这三者的正则完全不一样。搞清需求再动手是正则项目里少踩坑的第一步。3.3 误区三在字符集里乱用连字符[1-9]里的-是范围连接符但它的位置很讲究。-出现在字符集的中间表示“从哪到哪的范围”如果出现在开头或者结尾它不再表示范围而是表示字面意义的连字符-。比如[-1-9]表示匹配连字符、1到9任意一个[1-9-]也是表示匹配1到9或者连字符。如果你想匹配“数字1到9以及减号”直接写[1-9-]或者[1-9\-]都行但把范围连字符放在中间时如果写成[1--9]那就出大问题了此时引擎读到的可能是“从1到-再到9”语义混乱甚至直接报错。提示写字符集时如果包含多个范围用空格或换行分开肉眼也难辨最好把范围放前面、单字符放后面比如[1-9a-c]并且养成写注释的习惯否则三个月后回头看自己都不认识。3.4 误区四忘记全局标志g提取结果永远只有一条match方法在没有g标志时返回的数组结构完全不同。拿/[1-9]/去match(A1B2)得到的是[1, index: 1, input: A1B2, groups: undefined]这种带额外属性的数组而/[1-9]/g返回的是[1,2]。二者长度都不一样拿去遍历时很容易出bug。一般建议做提取用matchAll或带g的match做判断用test不要加g。既能各司其职又能避免正则有状态引发的一系列诡异问题。3.5 误区五把取反符号 ^ 和锚点 ^ 搞混字符集内部的开头^表示“非”比如/[^1-9]/匹配的是“除1到9之外的任意字符”。这个写法在过滤场景里很有用比如你想把文本里所有非数字字符替换掉可以写/[^0-9]/g配合空字符串替换。但它和锚点^行首匹配长得一样位置不同含义完全不同。/^[1-9]/中的^表示必须以1到9开头/[^1-9]/中的^表示排除1到9。一个放在字符集外一个放在字符集内初学者特别容易混。4. 实操从需求到正则可复制的三步拆解法4.1 第一步把需求翻译成“字符视角”写正则前先把中文需求翻译成引擎视角的描述。比如“匹配1到9”要翻译成“匹配一个字符这个字符的ASCII值在49到57之间”翻译得越具体正则越不会偏离。拿一个真实场景练手用户输入一个版本号格式是1.2.3每一段都不能以0开头段与段之间用点分隔。这个需求拆解成第一段第一位1到9后面跟0到若干个数字点字面量\.点要转义第二段、第三段同上。组合出来就是/^[1-9]\d*(\.[1-9]\d*){2}$/。搭出来的正则不但解决了“非零开头”的需求还兼顾了段数固定、点必须转义等细节。4.2 第二步根据场景选择匹配方法JS里正则和字符串方法搭配方式很多用途各不相同只要判断“是否匹配”用test不加g要提取所有匹配项用match加g或matchAll拿迭代器要全局替换用String.prototype.replace配合/g要分割字符串split也可以带正则比如str.split(/[,\s]/)。同一份正则放在不同方法里表现差异很大。我在提取一段文本中的数字时一开始用了exec循环后来发现直接match加g更妥帖但你要是想逐个处理匹配结果的上下文那exec循环反而更强。方法论上先明确“我要的是结果集还是操作流”再决定用哪个方法顺序不能反。4.3 第三步用真实数据验证边界值正则写出来基本都会“看起来不错”真正的魔鬼在边界值。针对[1-9]相关的场景至少拿这几组数据测一测空字符串确保不会出现意外匹配0、0.5确认0没有被错误接纳10、100验证量词和锚点是否正确01、00123看第一位0是否被正确拦截全角数字、中文数字一确认你不会把非ASCII数字误匹配。比如对表单校验/^[1-9]\d*$/测0返回false测01返回false测10返回true测1.5返回false这时基本可以认定正则满足需求。我见过太多人只拿一两个正常值测试就提交代码上线后被脏数据打得措手不及。4.4 完整示例从字符串里提取所有非零开头的整数假设有一段混合文本const text 苹果2个单价5.5元编号A12特价99尾货0处理; const result text.match(/[1-9]\d*/g); console.log(result); // [2,5,5,12,99]这个正则的含义是第一个字符必须是1到9后面可以跟任意个0到9。所以5.5会被拆成5和5两段因为点不是数字正则匹配到点就停了。如果不想把小数拆开就要用更复杂的模式比如小数点前后同时约束const result2 text.match(/[1-9]\d*(\.\d)?/g); console.log(result2); // [2,5.5,12,99]此时(\.\d)?表示小数部分整体出现0次或1次。但要注意0处理里的0不在结果里因为[1-9]排除了0作为首字符。如果业务上要求把0也保留为一个整数你需要再想其他方案。任何时候都不要期望一个字符集解决所有需求组合才是正则的核心能力。5. 性能、可读性与工具心得5.1 字符集比“并列分支”更高效有些新手在没学会字符集之前会写/(1|2|3|4|5|6|7|8|9)/这种表达式。语法上没错但无论从可读性还是性能上都不如[1-9]。字符集在引擎内部通常被编译成查找表或范围跳转匹配时一步就能判断而分支结构要逐个尝试每个分支分支数量多了以后回溯成本明显升高。更重要的是可维护性。/(1|2|3|4|5|6|7|8|9)/改动起来又臭又长[1-9]一行就能表达清楚。正则本身就是给人看的“可执行注释”写得越紧凑、越接近领域语义后续维护的人越省心。5.2 大量文本下也要留意反向过程如果你要对一个很大字符串做清洗比如把里面所有非数字字符替换成空通常直接写/[\D]/g或/[^0-9]/g。这里用上取反字符集往往比正向匹配再逐个替换高效。V8等现代引擎对字符集匹配有专门优化一段百万字符的文本跑下来差距可能不大但代码意图更清晰。一个经验正则不是越短越好而是“边界越明确越好”。你的[1-9]看似简单但配合锚点^和$它变成了一个强有力的验证工具配合*、、?量词它又能进化为提取工具。要想真的掌握它不能只记语法还得反复在真实场景里摩擦。5.3 调试工具与测试策略我在调正则的时候习惯用一个带可视化解释的工具比如 regex101 和 regexper。regex101 能在右侧实时标出某个字符被哪个匹配单元消耗掉还能高亮捕获组调试[1-9]这类简单字符集时尤其直观。先把正则在工具里跑通再贴回代码能省下大量在断点里反复尝试的时间。测试策略上建议针对每个正则写一个极简断言集合不一定要引入完整测试框架一个assert就够了const assert require(assert); const re /^[1-9]\d*$/; assert.strictEqual(re.test(123), true); assert.strictEqual(re.test(0), false); assert.strictEqual(re.test(01), false); assert.strictEqual(re.test(), false);把边界值固化下来以后需求变动、正则升级时运行一遍就知道有没有破坏既有逻辑。这比临时写几个console.log靠谱得多。5.4 延伸一点字符集与Unicode的隐患JavaScript 正则默认按 ASCII 理解\d、\w这些简写。[1-9]只匹配 ASCII 的1到9不会去匹配全角数字。如果你的输入可能来自中文输入法、复制粘贴用[1-9]时要注意这个前提。如果确实想匹配全角数字需要单独把全角数字的 Unicode 范围也加进去或者先做字符串规范化把全角转半角然后再走正则。这个坑在涉及用户输入、文本抓取时经常出现。6. 我的个人体会[1-9]目前是我写数字校验时用到的高频字符集之一但我已经不太会单独用它了。遇到“非零开头正整数”就是/^[1-9]\d*$/遇到“提取个位数字”就是/[1-9]/g遇到“过滤非数字”就是/[^0-9]/g。字符集往左一步是范围、往右一步是取反组合方式变一变需求就又变了一层。最后给一个持续有用的建议正则表达式不要靠死记硬背而是每写完一个都顺手记录下需求、正则、测试样例三件套。过两个月回看你会发现原本看不懂的表达式一下子变得亲切自己的踩坑清单也会越来越短。正则这玩意理解“匹配一个字符”这个本质之后剩下的就是不断拆需求、写组合、试边界的过程。
