input 只能输入数字怎么写:type=number 的坑与 composition 事件处理

type="number" 允许输入 e、滚轮会改值、maxlength 失效——它不是为输入校验设计的。本文给出 type="text" + inputmode + input 事件正则过滤的完整方案,重点讲清中文输入法 composition 事件的处理时机,附可直接复制的 bindNumberOnly 实现。

约 6 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

结论:想要「纯数字输入」就别依赖 type="number"

type="number" 的设计目标是「浏览器辅助输入数值」,不是输入校验:它允许 e/E 和 +/-,滚轮会改值,maxlength 失效,取到的 value 仍是字符串。要做真正的数字过滤,用 type="text"(配 inputmode)+ JS 过滤。

type="number" 的问题清单

这些行为都是规范规定的,改不掉,只能绕开。

type="number" 的行为带来的问题
允许输入 e / E(科学计数法合法)用户看到「能输入 e」,校验逻辑容易漏判
允许 + 和 - 前缀正负号混进业务字段(如手机号)
滚轮悬停会增减数值填表时误滚一下,数据悄悄变了
maxlength 不生效想限 11 位手机号做不到,得用 JS
step 不匹配时表单校验静默失败value=1.5 配 step=1 直接 invalid,无提示
粘贴非数字有时被静默清空用户以为输入了,实际 value 为空

基础版:input 事件 + 正则过滤

监听 input 而不是 keydown:keydown 拦不住粘贴、拖拽和输入法,input 事件在值变化后触发,处理的是最终结果。

只允许整数
const input = document.querySelector('#qty');
input.addEventListener('input', () => {
  // 把所有非数字字符替换为空
  const cleaned = input.value.replace(/\D/g, '');
  if (cleaned !== input.value) input.value = cleaned;
});

中文输入法:必须处理 composition 事件

中文输入法在「组词」过程中会反复触发 input 事件,事件里带着拼音中间态;如果在此时过滤,可能出现删字错乱、候选词被吞。正确做法:compositionstart 到 compositionend 之间不动值,end 之后再统一清理。

兼容输入法的过滤
let composing = false;
input.addEventListener('compositionstart', () => composing = true);
input.addEventListener('compositionend', () => {
  composing = false;
  clean(); // 组词结束后再做一次清理
});
input.addEventListener('input', () => { if (!composing) clean(); });

function clean() {
  const cleaned = input.value.replace(/\D/g, '');
  if (cleaned !== input.value) input.value = cleaned;
}

支持小数:四步写出健壮过滤

允许一个小数点比看起来麻烦:要防连续两个点、防点在开头、防清空后只剩「.」。

  1. 用 replace(/[^\d.]/g, '') 删除数字和小数点以外的所有字符
  2. 只保留第一个小数点:用 indexOf('.') 拆成整数与小数两段再拼回
  3. 修正孤立点:'.5' 补零成 '0.5',只剩 '.' 时清空为 ''
  4. 监听 blur 做兜底校验,最终值交给业务前再 Number() 验证一次

可直接复制的完整实现

整数 / 小数通吃
function bindNumberOnly(input, { decimal = false } = {}) {
  const pattern = decimal ? /[^\d.]/g : /\D/g;
  let composing = false;

  const clean = () => {
    let v = input.value.replace(pattern, '');
    if (decimal) {
      const i = v.indexOf('.');
      if (i !== -1) v = v.slice(0, i + 1) + v.slice(i + 1).replace(/\./g, '');
      if (v === '.') v = '';
      else if (v.startsWith('.')) v = '0' + v;
    }
    if (v !== input.value) input.value = v;
  };

  input.addEventListener('compositionstart', () => composing = true);
  input.addEventListener('compositionend', () => { composing = false; clean(); });
  input.addEventListener('input', () => { if (!composing) clean(); });
  input.addEventListener('blur', clean); // 失焦兜底
}

// 用法
bindNumberOnly(document.querySelector('#phone'));                    // 纯整数
bindNumberOnly(document.querySelector('#price'), { decimal: true }); // 允许小数

容易忽略的四个细节

功能跑通之后,线上问题往往出在这些边角。

  • 光标跳动:从中间删改时直接改 value 会跳到末尾,重要表单要记录 selectionStart 再恢复
  • inputmode="decimal" 只影响移动端弹出的键盘类型,桌面端照样能输入字母,两者要配合用
  • maxlength 对 type="number" 无效,换 type="text" 后记得补上长度限制
  • 过滤后值可能为空串,提交前要区分「用户没填」和「填了但非法」两种提示

粘贴处理与推荐 HTML 写法

paste 事件里可以拿到剪贴板纯文本,提前清洗再写回;HTML 侧用 inputmode 引导移动端键盘类型。

HTML 写法
<!-- type 保持 text,inputmode 引导移动端数字键盘 -->
<input type="text" id="price" inputmode="decimal" maxlength="12">
粘贴清洗
input.addEventListener('paste', (e) => {
  e.preventDefault();
  const text = (e.clipboardData || window.clipboardData).getData('text');
  // 粘贴内容统一走同一套清洗逻辑
  input.value = text.replace(/[^\d.]/g, '');
  input.dispatchEvent(new Event('input'));
});

一行修复:type="number" 换成 type="text" + inputmode

把 <input type="number"> 改成 <input type="text" inputmode="decimal">,滚轮改值、e 字符、maxlength 失效三个问题同时消失;再接上面的过滤函数兜底。正则写不准时,先在正则测试工具里跑一遍 /[^\d.]/g 和边界用例,正则速查表里有完整的数字校验表达式。

常见问题

为什么 type="number" 能输入字母 e?

HTML 规范允许 number 输入浮点数语法,含科学计数法的 e/E,这是特性不是 bug。需要严格数字过滤请改用 type="text" 加 JS。

中文输入法下过滤为什么会乱?

组词过程中 input 事件携带拼音中间态,直接改 value 会破坏候选词。需用 compositionstart/end 标记组词期间跳过过滤,end 后统一清理。

inputmode="decimal" 能代替 JS 过滤吗?

不能。它只影响移动端虚拟键盘的布局,桌面浏览器仍可输入任意字符,必须与 JS 过滤配合使用。

滚轮改动 number 输入框的值能关掉吗?

可以监听 wheel 事件并 preventDefault,但更彻底的办法是改用 type="text",问题根源就不存在了。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件