做前端的谁还没跟输入框较劲过几次。刚接手后台管理系统那会儿我第一次任务是给一堆input加输入限制——有的只能输数字有的要限制中文字符有的要过滤掉标点符号弄来弄去发现核心全落在“JS 怎么拦、正则怎么写”这两件事上。这篇文章就把我实际项目中常用于控制 input 输入数字、汉字与普通字符的方案完整拆开讲一遍适合刚接触前端、或者正则用得还不够顺手的同学也适合想把手头表单校验做得更稳的开发者参考。控制输入框听起来就是监听几个事件、写几个if判断的事但一旦遇到中文输入法、粘贴、光标跳动这类实际问题很多初学者容易卡住。我的做法是用事件来拦截“输入动作”用正则来决定“什么能留、什么该删”两者配合才能做到既挡得住脏数据又不伤用户体验。1. 输入控制这件事本质是在解决什么问题1.1 你以为只是拦一下键盘其实是在挡脏数据很多业务场景里输入框的内容最终会提交到后端比如手机号、身份证号、金额、姓名、订单编号。前端如果不加限制用户既可以在数字框里输入字母也可以在姓名框里粘贴一串特殊符号。等到提交的时候再做整体校验也不是不行但用户体验很糟糕——用户都填完了才提示“格式错误”那就只能一遍遍修改。输入控制的本质价值有两个第一是预防在用户输入过程中就把非法内容拦掉避免脏数据进系统第二是反馈让用户立刻知道自己输入了不被允许的内容而不是等到提交时报错。我的经验是能用前置拦截解决的就不要拖到提交后才校验。这也是为什么“输入时实时用正则过滤”比“提交时统一校验”更常用的原因。但这里要说明一句前端控制永远只是辅助它不是安全机制。真正入库的数据后端必须再校验一次。前端控制做得好是为了体验不是为了防住恶意用户这个定位要先搞清楚。1.2 事件方案怎么选keydown、input、change 分别管什么控制输入内容可用的浏览器事件有好几个我实际工作中大致是按这样的分工来理解keydown用户按下物理按键时触发适合拦截单个按键比如禁掉字母 e但管不住粘贴和中文输入法上屏的内容。input输入框内容实际变化时触发覆盖键盘输入、粘贴、拖拽、自动补全、中文输入法上屏是实时控制输入的最主要事件。change输入框失焦时触发适合做最终校验不适合做逐字拦截。compositionstart/compositionend/compositionupdate中文输入法组合输入时触发专门用来识别“用户正在用拼音打汉字”的阶段避免在拼音组合中间误删内容。最常用、最稳的组合是监听input事件在内容变化后立刻用正则检查/修正同时配合一个“是否正在拼音输入”的状态标志来避免误伤中文输入过程。keydown不能作为主力工具因为它只发生在物理按键阶段粘贴来的内容它根本看不到。1.3 为什么正则表达式是这套方案的核心事件只是给了我们一个“什么时候处理”的时机真正决定“哪些字符合法、哪些该被过滤”的是正则表达式。用一个简单例子说明// 只允许整数 const INT_REG /^\d$/;判断当前的输入框值是否符合要求用INT_REG.test(value)就能得到 true/false。如果想过滤掉非法字符就要用value.replace(/[^\d]/g, )把非数字字符全部替换成空字符串。正则的价值在于它可以灵活定义“合法字符集合”。数字是一类汉字是一类英文字母是一类特殊符号又是一类。用户需求通常不是单纯的“全都要数字”而是“数字可以但小数点只能有一个”“汉字可以但全角标点不行”。这些细化条件都是靠正则的元字符、字符组、量词和修饰符来完成的。选正则还有一个好处是代码量小、可读性好。一个需求一两行正则就写完了比写一长串if (char 0 || char 1 || ...)优雅得多。这个项目标题里之所以把“数字、汉字、字符”和“正则表达式”并列其实就是因为正则才是真正实现约束的核心工具。2. 正则表达式基础数字、汉字、字符到底怎么表达2.1 先看几个最常用的匹配写法控制输入框本质上是在构建一个“字符串是否符合预期格式”的判断逻辑。这里我先列几个业务里最高频的写法后面实操会反复用到。// 纯数字整数 const digitsOnly /^\d$/; // 数字 小数点最多保留两位小数 const decimalReg /^\d(\.\d{1,2})?$/; // 无符号数兼容 .5 和 1. 这种输入过程值 const numberLikeReg /^\d*\.?\d*$/;\d代表数字表示至少一个?表示可选{1,2}表示一到两个。$分别约束字符串从哪开始、到哪结束。这种正则写出来对“值已是定稿”的校验非常有效。但有一个细节得特别注意用户输入是一个过程过程中间的值是残缺的。比如我准备输入“12.5”当输到“12.”时/^\d(\.\d{1,2})?$/判断这个字符串是合法的因为小数部分在整个正则里是可选的。但如果用户先输入了“.”那这个值就不合法了。实际开发中对不同输入阶段的值我们会额外兼容一些中间态这属于实操经验后面专门讲。汉字在正则里通常用 Unicode 范围来表示最经典的是// 全角汉字常用基本区 const chineseReg /^[\u4e00-\u9fa5]$/;这里的\u4e00到\u9fa5覆盖了大多数常用汉字。不过要注意它不包含扩展汉字区、生僻字以及一些少数民族文字如果业务需要兼容更广的字符集可以使用\p{ScriptHan}配合u修饰符。不过在大多数中后台项目里[\u4e00-\u9fa5]已经完全够用。2.2 常用输入约束正则对照表有了基础认识我给你整理一张我平时写表单时的高频对照表可以直接参考业务需求正则示例说明纯整数/^\d$/不允许小数和负号整数可负/^-?\d$/负号最多出现在开头最多两位小数金额/^\d(\.\d{1,2})?$/常用于金额输入任意小数/^\d*\.?\d*$/输入过程兼容用纯汉字/^[\u4e00-\u9fa5]$/姓名、汉语文本场景中英文汉字混合/^[\u4e00-\u9fa5a-zA-Z]$/去掉空格和符号字母数字下划线/^\w$/常用于用户名、订单号手机号/^1[3-9]\d{9}$/以 1 开头第二位 3-9不含空格/^\S$/禁止任何空白字符过滤非数字字符/[\^\d]/g配合 replace 使用需要说明的是正则里的\w在 JavaScript 中默认包含[a-zA-Z0-9_]某些语言环境还会包含 Unicode 字符但在 JS 中它是 ASCII 范围内的。真正使用时最好按需求显式写出字符集合。2.3 中文输入法为什么是最大的坑如果只做英文和数字输入处理起来很简单因为每个字母都是直接上屏的。但中文输入法不一样用户输入拼音时input事件会在组合过程中不断触发值会临时变成输入的拼音字母。例如我输入“你好”这个过程可能是输入“nihao”之后再按下候选键才真正把“你好”填入输入框。如果你在input事件里写的是“只允许汉字”那么用户在打拼音阶段就会被replace把拼音字母全删掉根本没法完成中文输入。这就是“汉字输入控制”和“英文字符输入控制”最大的不同。解决办法是锁定输入法组合状态监听compositionstart设置一个标志位compositionend再取消。在input事件里判断标志位如果用户正处于拼音组合阶段就暂时不做任何过滤let isComposing false; inputEl.addEventListener(compositionstart, () { isComposing true; }); inputEl.addEventListener(compositionend, (e) { isComposing false; // 组合结束后再执行一次过滤确保最终上屏内容合规 filterInput(e.target.value); });这段逻辑我在多种浏览器上都验证过是目前兼容性比较好的方案。此外input事件对象上还有一个e.isComposing属性大多数现代浏览器都支持可以直接用来判断但为了稳妥我习惯自己维护一个标志位。3. 实操用 JS 正则实现一个可控的输入框3.1 只允许数字输入整数、小数、负号怎么处理先做最简单的需求“只能输入数字”。很多人第一反应是用typenumber但typenumber在浏览器里表现并不统一它允许输入e、、-、.这些符号而且在部分浏览器里中文输入法还能输入汉字。所以我的习惯是不用typenumber做输入控制而是用typetext自己控制只有最终展示成数字格式时才用typenumber。给一个事件处理函数function setupNumberInput(inputEl) { inputEl.addEventListener(input, () { let value inputEl.value; // 先去掉所有非数字和非小数点的字符 value value.replace(/[^\d.]/g, ); // 只保留第一个小数点 const parts value.split(.); if (parts.length 2) { value parts[0] . parts.slice(1).join(); } // 限制小数位数为两位 if (parts[1] parts[1].length 2) { value parts[0] . parts[1].slice(0, 2); } inputEl.value value; }, { // 避免使用 toLocaleString 等手段自动改写值后导致光标移动 passive: true }); }这段代码里有一个关键取舍我并不是简单地用一行replace(/[^\d.]/g, )而是还处理了小数点和位数。因为如果只删非法字符“1.2.3”这种值依然会存在而后端解析时就可能出现问题。多写两行代码把值稳定成“看起来合法”的格式是控制输入框的一个常用策略。如果需求还允许负数就要在开头放行一个负号value value.replace(/[^\d.\-]/g, );但要注意负号只应出现在最前面写了正则也只能过滤非法字符规则判断还是得靠后面的逻辑。自定义提示建议写成 KISS 原则数字控制框允许输入0~9、小数点、负号其余全部拦截小数点最多一位负号只能在首位。这样规则简单用户也好理解。3.2 只允许汉字输入直接写正则就行吗“只允许汉字”这个需求在通讯录、报名表、处方信息里很常见。对应的正则代码并不复杂function setupChineseInput(inputEl) { let isComposing false; inputEl.addEventListener(compositionstart, () { isComposing true; }); inputEl.addEventListener(compositionend, (e) { isComposing false; filterChinese(e.target); }); inputEl.addEventListener(input, (e) { if (!isComposing !e.isComposing) { filterChinese(e.target); } }); function filterChinese(target) { target.value target.value.replace(/[^\u4e00-\u9fa5]/g, ); } }直接写正则确实可以但上面这段代码里真正重要的是对中文输入法组合期的判断。没有这段判断你在输入拼音时会把拼音字母当成非法字符全清掉体验直接崩掉。这也是所有汉字输入控制里最常见的坑所以我还是单独拿出来强调一次。另外姓名场景有时需要允许姓氏里的多音字、生僻字[\u4e00-\u9fa5]覆盖不了就需要把范围放宽到\u3400-\u4DBF扩展A区或者干脆用.代替精确汉字范围再做黑名单过滤。黑名单的思路是只禁止明确不该出现的字符而不是精确要求只出现哪些字符这在某些开放输入场景中更实用。3.3 白名单/黑名单字符过滤控制“字符”的两种思路“数字、汉字、字符”里的“字符”指的是英文字母、标点、空格这些常规内容。控制字符的方案一般有白名单和黑名单两种路线。白名单的思路是我明确指定哪些字符可以出现其余全部删除。比如只允许英文字母和数字inputEl.addEventListener(input, (e) { const target e.target; target.value target.value.replace(/[^a-zA-Z0-9]/g, ); });黑名单的思路是我明确指定哪些字符必须删除其余全部保留。比如禁止输入 这类容易破坏页面结构的字符inputEl.addEventListener(input, (e) { const target e.target; target.value target.value.replace(/[]/g, ); });两种思路适用场景完全不同。白名单适合格式严格、场景明确的输入比如订单编号、邀请码、手机号黑名单适合内容开放的输入比如备注、地址只需要防住个别危险字符。我的建议是能用白名单的地方尽量用白名单因为“只给特定字符开口子”比“排除一部分字符”要安全得多也容易维护。这里还要讲一个细节正则里的字符组开头加^表示“否定”/[^a-zA-Z0-9]/意思是“所有不在 a-zA-Z0-9 范围内的字符”。这个语法在过滤场景中非常高频率使用一定要记牢。3.4 组合成一个通用函数绑到不同 input 上如果项目里有很多输入框各自规则不同我不建议给每个输入框单独写事件处理函数那样代码会非常冗余。我习惯封装成一个通用工具函数用配置项来控制限制类型function setInputFilter(inputEl, options) { const { allow all, // all, number, int, chinese, alphanumeric, custom allowChars , // 额外允许的字符 blackChars , // 黑名单字符 maxLen Infinity, allowDecimal false, allowNegative false, decimalPlaces 2 } options; function buildPattern() { switch (allow) { case number: return allowNegative ? /[^\d.\-]/g : /[^\d.]/g; case int: return allowNegative ? /[^\d\-]/g : /[^\d]/g; case chinese: return /[^\u4e00-\u9fa5]/g; case alphanumeric: return /[^a-zA-Z0-9]/g; case custom: { // 动态生成白名单正则 const allowedEscaped allowChars.replace(/[.*?^${}()|[\]\\]/g, \\$); return new RegExp([^${allowedEscaped}], g); } default: return null; } } const pattern buildPattern(); const blackPattern blackChars ? new RegExp([${blackChars}], g) : null; inputEl.addEventListener(input, (e) { if (e.isComposing) return; let value inputEl.value; if (pattern) { value value.replace(pattern, ); } if (blackPattern) { value value.replace(blackPattern, ); } if (options.allowDecimal !allowNegative) { // 只保留第一个小数点、限制小数位数 const parts value.split(.); if (parts.length 2) value parts[0] . parts.slice(1).join(); if (parts[1] parts[1].length decimalPlaces) { value parts[0] . parts[1].slice(0, decimalPlaces); } } if (value.length maxLen) { value value.slice(0, maxLen); } if (value ! inputEl.value) { inputEl.value value; } }); }这套封装的好处是业务层调用起来非常简洁比如setInputFilter(document.querySelector(#amount), { allow: number, allowDecimal: true, decimalPlaces: 2, maxLen: 10 }); setInputFilter(document.querySelector(#name), { allow: chinese }); setInputFilter(document.querySelector(#code), { allow: alphanumeric, maxLen: 20 });代码里动态构建正则时我用replace(/[.*?^${}()|[\]\\]/g, \\$)对允许字符做了转义处理这一步很重要。因为如果 allowChars 里带了.或*不转义的话正则元字符会改变匹配语义甚至造成规则失效。新手很容易在这种地方踩坑。3.5 处理粘贴、拖拽和自动补全只用键盘输入时input事件足够用了。但用户还有三条“输入通道”是很多初学开发者容易忽略的粘贴、拖拽文字、浏览器自动补全。这几类操作都不会触发keydown但都会触发input事件所以只要用了input事件这些场景就已经被覆盖到了。不过覆盖到了“事件触发”不代表能正确“读取数据”。比如粘贴的时候如果你想在拦截的同时知道用户粘了什么东西得在paste事件里通过clipboardData读取剪贴板内容而input事件里拿到的只有变化后的整体值inputEl.addEventListener(paste, (e) { const text e.clipboardData.getData(text); // 这里可以对 text 做预检查甚至阻止默认行为 if (/[^\d]/.test(text)) { e.preventDefault(); } });我个人建议采用“整体值过滤”而不是依赖paste拦截因为前者代码简单逻辑统一。粘贴后值若包含非法字符在input事件里照样会被清掉。只有当你需要“粘贴时弹窗提示”这种更强交互时才需要单独处理paste事件。浏览器自动补全更需要重视。很多用户浏览器会记住历史输入值点选补全后同样触发input事件所以整体过滤那条链路对自动补全依然有效。唯一要提醒的是不要在 input 事件里频繁把值重置成空字符串否则自动补全选项刚点完就被清掉用户会以为浏览器坏了。我的做法是过滤后再赋值如果过滤结果为空就保留空值但不要打断浏览器的补全面板交互。4. 常见问题与排查技巧实录4.1 光标跳动replace 值之后光标跑到了最后这是控制输入框时最高频的问题。输入中间内容时比如值原来是1234光标停在1和2之间此时输入字母“x”如果你直接把新的过滤值赋值给 inputEl整个值从1x234变成1234浏览器会默认把光标移动到末尾用户想再在中间插入内容就得重新定位体验很差。处理思路是记录光标位置赋值后恢复inputEl.addEventListener(input, () { const start inputEl.selectionStart; const end inputEl.selectionEnd; const newValue inputEl.value.replace(/[^\d]/g, ); if (newValue ! inputEl.value) { inputEl.value newValue; // 重新设置光标位置注意结尾越界问题 const pos Math.min(start, newValue.length); inputEl.setSelectionRange(pos, pos); } });这个逻辑我一般会在过滤函数里统一封装。如果每次都做光标恢复代码会多几行但用户感知明显提升。尤其是“引用中间数字再输入”的场景不改这个会让用户崩溃。4.2 拼音上屏瞬间被拦composition 事件是关键前面已经提过几次了再系统讲一遍。中文输入法拼音组合过程中会触发input事件所以如果用“值过滤”的逻辑处理拼音字母会被正则当场删掉。我实际项目中有个输入电话号码的输入框因为只允许数字用户用中文输入法打数字时拼音拼音“yi”会先上屏为英文字母y和i然后被过滤逻辑当成非法字符删除最终用户根本输不了中文数字。解决顺序是这样的inputEl.addEventListener(compositionstart, () { isComposing true; }); inputEl.addEventListener(compositionend, (e) { isComposing false; filterValue(e.target); }); inputEl.addEventListener(input, (e) { if (!isComposing !e.isComposing) { filterValue(e.target); } });compositionend之后一定要执行一次过滤因为此刻输入框里已经是最终的中文字符了。如果在组合结束时不执行就可能把拼音组合期间漏掉的非法字符留在输入框里。4.3 typenumber 的隐藏坑很多项目里默认用input typenumber来限制数字输入它的确会在大多数浏览器上弹起数字键盘但问题也不少iOS 上typenumber支持不够一致会出现样式和事件差异。部分浏览器允许输入e、E、、-和.这些本身是科学计数法和负数的符号但对普通业务来说通常不想要。input事件在某些浏览器里拿e.target.value时如果值不规范可能直接变成空字符串导致你无法感知用户输入了什么。正则校验^\d$会对typenumber内部的值产生兼容性差异因为value可能被浏览器规范化。我的建议是颜值要求不高、格式要求严格的项目全部用typetext 正则控制只有在需要移动端弹出数字键盘的场景才用inputmodedecimal补充。事实上现在inputmode能很好地兼顾“不触发浏览器内置校验”和“弹出数字键盘”两个需求比typenumber干净得多。4.4 失焦兜底校验不能省实时过滤能挡掉 90% 的非法输入但拦不住所有情况。比如用户通过开发者工具改值、通过浏览器插件填表、或者在组合输入状态下直接点击提交按钮导致compositionend还没来得及触发这时输入框里可能出现漏网之鱼。所以我习惯在blur失焦事件里再做一次校验把最终值再过滤一遍。这样做成本低、收益稳inputEl.addEventListener(blur, (e) { const target e.target; target.value target.value.replace(/[^\u4e00-\u9fa5]/g, ); });失焦过滤还有一种额外好处如果用户输入了不规范但勉强能解析的值比如“1.”失焦时可以顺手格式化成一个更规范的值比如“1”。这个过程只会一次性发生不会打扰用户输入过程中的体验。4.5 常用问题速查表问题现象根本原因处理方式中文输入时拼音被清掉未处理 composition 事件加 compositionstart/compositionend 标志过滤后光标跳到末尾直接赋值导致光标丢失记录 selectionStart 再 setSelectionRange粘贴的内容没被过滤只监听 keydown改用 input 事件或单独处理 pastetypenumber 下 e、- 拦不住浏览器内置行为改用 text inputmode只允许两位小数但用户输入 1.2.3只删非数字未处理重复小数点按小数点 split只保留第一个自动补全被瞬间清空input 事件中大量重置为空过滤后再赋值不主动打断补全输入过程中值异常为空typenumber 的值规范化使用 typetext动态正则语法报错allowChars 含特殊元字符先对字符做正则转义这张表基本覆盖了我这几年前端开发里遇到的各种“输入框控制”异常。遇到新问题时先对照“监听的是哪个事件、正则匹配的是什么范围、值何时被改写”这三个维度排查大概率能快速定位。我个人在实际开发中的习惯是把过滤逻辑收敛到一个函数里然后用配置对象驱动所有输入框而不是在 HTML 里写一堆oninput属性。这样不管是新增规则还是排查问题都只需要改一处。最后再分享一个小技巧写完正则别急着上线把abc123汉字这类混合字符串丢进去跑一遍看看过滤结果是否符合预期这个习惯能帮你省掉很多测试工单。