ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

表单验证为何失效?onSubmit中的return到底多重要

表单验证为何失效?onSubmit中的return到底多重要 刚接手一个老项目看到form onsubmitreturn checkForm()这种写法时我愣了一下——为什么这里非要写return紧接着我就想到自己刚开始写前端时的经历有段时间我写onsubmitvalidate()明明函数里也写了return false表单还是不听话地提交刷新页面后数据全丢。这个坑几乎每个接触表单的人都会踩一次但很少有人愿意把它掰开揉碎讲清楚。其实onsubmit和return是表单前端验证路上最开始、也最容易被忽视的一道坎。理解了它们在浏览器里到底是怎么协作的后面再写验证逻辑就顺多了排查问题也不会一脸懵。这篇文章就围绕这两个关键字展开把内联事件机制、返回值的作用、异步验证的坑、防重复提交的写法一次说透适合所有写过表单但没深究过底层逻辑的前端开发者。1. 一个经典困惑少写一个 return验证为什么就失灵了1.1 先复现一次“失灵”现场先看一段最典型的错误代码form onsubmitvalidateForm(event) input typetext idusername / button typesubmit提交/button /form script function validateForm(e) { var username document.getElementById(username).value; if (username.length 3) { alert(用户名至少3个字符); return false; } } /script看起来逻辑没毛病当用户名太短时validateForm返回了false按“常识”应该阻止提交。但实际运行后你会发现alert弹出来了点掉之后表单照样提交页面还是刷新了。为什么问题就出在onsubmit这个属性本身的写法上。你写的是onsubmitvalidateForm(event)这相当于告诉浏览器提交表单的时候执行一句validateForm(event)。注意这里没有return所以这句代码的结果是什么是validateForm的返回值吗不是。一个没有return的函数调用表达式它的值会被丢弃事件处理代码最终整体返回的是undefined。浏览器在决定“要不要提交”的时候看的是事件处理器有没有明确返回false。你返回的是undefined自然就当作“没有说要阻止”于是继续提交。只要在调用函数前加一个returnform onsubmitreturn validateForm(event)浏览器执行的就变成了return validateForm(event);validateForm返回的false才会被作为整个事件处理代码的返回值也才能真正阻止表单提交。1.2 浏览器到底怎么处理 onsubmit 属性这里需要理解一个底层细节类似onsubmit这样的内联事件属性在浏览器内部并不是简单地“执行一段代码”而是会被编译器包装成一个匿名函数。大致可以理解为form.onsubmit function (event) { // 这里放的是你在 HTML 属性里写的代码 validateForm(event); };当你写onsubmitvalidateForm(event)包装后就是上面那样函数体最后一句是调用但没有显式return所以匿名函数返回undefined。当你写onsubmitreturn validateForm(event)包装后就是form.onsubmit function (event) { return validateForm(event); };这样匿名函数就会把validateForm的返回值原样抛出去。对于onsubmit这类事件如果事件处理函数的返回值严格等于false浏览器就会取消该事件的默认行为——“提交表单”这个动作就被拦下来了。这就是return的唯一使命把你的验证函数得出的布尔结果透传给浏览器的“默认行为决策层”。2. return 到底干了什么从事件处理机制的视角看验证流程2.1 返回 false 和调用 preventDefault 的关系你可能见过另一种写法在事件处理函数里调用event.preventDefault()也能阻止提交。那么return false和preventDefault有什么区别在标准的事件监听器模型比如addEventListener下事件回调的返回值是完全没有意义的浏览器不会因为它返回false就阻止默认行为。你必须显式调用event.preventDefault()。但内联事件属性不一样。浏览器在实现内联事件处理器时保留了一套“远古协议”如果处理函数的返回值是false就自动帮你调用preventDefault()。所以form onsubmitreturn false;其实就等价于form.addEventListener(submit, function (event) { event.preventDefault(); });这也是为什么内联事件里return false能如此简洁地阻止提交。你可以把onsubmit的事件处理函数想象成一个隐藏的“推荐官”浏览器问“这个表单要提交吗”处理函数说“我返回false”——浏览器就知道你要否决于是不提交返回其他任何值浏览器都按默认动作走。2.2 验证函数内部return 是控制流的“闸门”除了透传给浏览器的布尔值return在验证函数本身里还有一个重要作用提前结束函数执行。编写一个典型的验证函数时常规结构是自上而下逐项检查function validateForm() { var username document.getElementById(username).value; var email document.getElementById(email).value; if (username.trim().length 3) { alert(用户名太短); return false; // 到这里就结束了后面的代码不再执行 } if (email.indexOf() -1) { alert(邮箱格式不对); return false; // 提前返回防止继续执行下面的提交逻辑 } return true; // 所有检查都通过 }这里的return false相当于“短路”一旦某项检查失败函数立刻停止不会再浪费时间验证后面的字段更不会跑到最后的return true。如果忘了写return可能出现一种诡异情况第一个字段验证失败弹了提示第二个字段验证也失败也弹提示最后函数却没有整体返回false表单照常提交。所以在验证函数内部return既是结果输出也是流程控制。你可以理解为流水线上的质检员发现一个次品马上叫停整条流水线而不是等所有质检项都过一遍再统一汇报。2.3 为什么验证函数最后必须有个 return true很多新手会疑惑验证失败时返回false阻止提交那验证成功时需要显式返回true吗需要。因为按照事件处理协议只有返回false才会取消默认行为。如果验证成功时你不返回函数默认返回undefined浏览器也会放行提交。从结果上看不写return true好像也没问题。但问题在于如果你的函数结构里没有明确的return true将来某天不小心加了一段代码把返回值覆盖了就会埋雷。更重要的是当你的验证成功并准备继续提交时可能还需要在函数里做一些前置操作比如禁用按钮、改变按钮文字。这些操作都放在return true之前形成一条明确的通过路径。写代码时养成“每个分支都有明确 return”的习惯读起来清清楚楚。3. 一个能直接上线的注册表单onsubmit return 的完整实战3.1 页面结构设计空谈原理不如动手。我用一个注册表单为例把前面说的东西串起来。这个表单包含用户名、邮箱、密码、确认密码四个字段外加一个防重复提交的控制逻辑。先写 HTMLform idregisterForm onsubmitreturn handleRegister(this) novalidate div label用户名/label input typetext nameusername placeholder3-16个字符 / /div div label邮箱/label input typetext nameemail placeholderexampledomain.com / /div div label密码/label input typepassword namepassword placeholder至少6位 / /div div label确认密码/label input typepassword nameconfirm / /div button typesubmit idsubmitBtn注册/button /form这里我刻意加了novalidate为什么因为如果不加浏览器会先用自己的原生校验规则处理typeemail、required等属性。原生校验一旦失败submit 事件根本不会触发那我们的onsubmit连执行的机会都没有。在自定义验证体系的场景下通常先用novalidate关掉原生校验把控制权完全交给自己。后面章节我会单独讲什么时候该用原生校验。3.2 验证函数的完整实现接下来是 JS 部分function handleRegister(form) { var username form.username.value.trim(); var email form.email.value.trim(); var password form.password.value; var confirm form.confirm.value; // 用户名3~16个字符 if (username.length 3 || username.length 16) { alert(用户名长度需在3到16个字符之间); return false; } // 邮箱简单格式校验保证有 和域名 var emailReg /^[^\s][^\s]\.[^\s]$/; if (!emailReg.test(email)) { alert(请输入正确的邮箱地址); return false; } // 密码至少6位 if (password.length 6) { alert(密码至少6位); return false; } // 确认密码与密码一致 if (password ! confirm) { alert(两次输入的密码不一致); return false; } // 所有验证通过进入“提交中”状态 var submitBtn form.querySelector(button[typesubmit]); submitBtn.disabled true; submitBtn.textContent 提交中...; return true; }细节体会一下form.username是直接通过表单的name属性访问输入框的快捷方式不需要再写getElementById。用户名和邮箱都用了.trim()把用户误输入的前后空格去掉。这一步在验证前做能避免“明明是 xiaoming因为多了一个空格被判不符合格式”这种无谓拦截。每个失败分支都return false且提示完之后立刻终止函数。所有验证通过后在return true之前把按钮禁用防止用户手抖连点。如果这里是同步表单提交页面马上会跳转按钮状态不需要恢复如果走 AJAX 提交后续要在请求结束后恢复按钮。3.3 为什么防重复提交要放在 return true 之前有人会把禁用按钮的代码放在onsubmit属性里比如写成onsubmitthis.btn.disabledtrue; return handleRegister(this)。这也能用但我更推荐放在验证函数内部、且放在return true之前的最后一步。原因很简单只有当验证通过、确定要放行提交的时候才应该进入“提交中”状态。如果验证失败函数提前return false按钮保持可点击用户可以立即修正重新提交。如果一开始就把按钮禁用验证失败时你还得想着恢复按钮多一重麻烦。如果你想更稳妥地防连点可以再加一个模块级标志变量var submitting false; function handleRegister(form) { if (submitting) { return false; } // ...各项验证 submitting true; return true; }submitting标志的好处是哪怕用户通过回车键触发表单提交也能被拦截。因为disabled的按钮不一定能阻止所有隐式提交场景标志变量直接卡在函数入口更可靠。4. 实战中“时灵时不灵”的表单验证高频踩坑与排查思路4.1 验证函数报错表单反而顺利提交这是最隐蔽、也最常见的问题之一。之前接手过一个反馈用户说“表单验证有时候会失效”。我打开控制台一看原来是某个字段的取值写错了比如form.userNmae拼写少了一个字母导致undefined上调用.value直接抛出Cannot read property value of undefined。一旦验证函数内部抛异常函数就不会执行到return false整个事件处理函数返回的是undefined浏览器照样提交表单。所以排查“验证失灵”问题时第一步永远是打开 DevTools 看 Console 面板。如果有一堆红色报错那表单大概率就“失控”了。报错不光是拼写问题还包括你引用了未定义的变量、使用了错误的 DOM 结构等等。养成习惯任何表单验证函数上线前先在控制台手动调用一次确认没有异常。4.2 回车键和多按钮的“偷跑”另一个容易忽略的场景是用户没点提交按钮而是在输入框里按了回车。浏览器会自动触发表单提交同样会走onsubmit流程。这本来是好事但如果你页面里有多个按钮其中某个按钮没有显式设置type属性它的默认类型就是submit。结果就是用户明明是点“取消”或“查询”之类的按钮却触发了整个表单的提交验证甚至直接提交了表单。正确姿势是表单内只要不是主动提交的按钮一律显式写明typebuttonbutton typebutton取消/button button typesubmit提交/button而主提交按钮我习惯显式写typesubmit不依赖默认值让别人看代码也一目了然。4.3 addEventListener 场景下 return false 会失效很多人会在onsubmit之外又用addEventListener给同一个表单绑定了额外的提交逻辑。这时候要特别注意addEventListener的回调函数里返回值是无效的。document.getElementById(registerForm).addEventListener(submit, function () { if (someCondition) { return false; // 无效不会阻止提交 } });这段代码里的return false不会阻止表单提交。因为标准事件监听器没有“返回值决定默认行为”这个机制。你必须调用document.getElementById(registerForm).addEventListener(submit, function (event) { if (someCondition) { event.preventDefault(); } });所以当你决定用addEventListener统一管理事件时就不要再依赖return false了。最怕的是同一个人一会儿用内联return一会儿用addEventListener一个表单两种风格排查起来非常难受。我的建议是选一条路走到底。如果项目是用原生 JS 且没引入框架内联onsubmitreturn fn()简单直接如果你在用 React/Vue 这类框架大概率不用亲手动onsubmit属性但在原生环境里这个机制依然很重要。4.4 异步验证最经典的“拦不住”用户名是否被占用、手机号是否已注册这类校验往往需要请求后端接口。于是有人会写出这样的代码function checkUsername() { fetch(/api/check) .then(res res.json()) .then(data { if (data.exists) { return false; // 这里返回 false 只能退出 then 回调 } return true; }); }然后onsubmitreturn checkUsername()。运行后发现无论接口返回什么表单都会提交。原因很明显——checkUsername执行到fetch之后不会等网络请求完成函数直接就结束了。此时它没有返回false而是返回undefined浏览器判定“没有阻止提交”于是表单已经飞出去了异步回调里的结果毫无意义。而且就算你想让checkUsername返回一个布尔值fetch的返回值是 Promise异步回调里的return根本影响不到外层函数。那异步验证该怎么做核心思路是先同步阻止这次提交再在异步结果出来后决定是否手动提交。一个可靠模式function handleSubmit(event, form) { // 先做所有同步验证 if (form.username.value.trim().length 3) { alert(用户名太短); return false; } // 需要异步检查用户名是否存在 var username form.username.value.trim(); event.preventDefault(); // 或者直接 return false先拦下这次提交 fetch(/api/check, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username: username }) }) .then(response response.json()) .then(data { if (data.exists) { alert(该用户名已被占用); } else { form.submit(); // 手动提交注意 submit() 不会触发 onsubmit } }) .catch(function () { alert(网络异常请稍后重试); }); return false; // 同步返回 false保证这次默认提交被阻止 }内联绑定写成form onsubmitreturn handleSubmit(event, this)这里有两个小知识点需要强调一是event.preventDefault()和return false在这里二选一即可我两个都写是为了让函数复制到addEventListener下也能正常工作。如果你坚持纯内联风格去掉event.preventDefault()只保留return false也完全没问题。二是form.submit()方法提交表单时不会触发onsubmit事件。这句话是双刃剑好处是异步验证通过后手动submit()不会重复走一遍验证坏处是任何地方直接调用form.submit()都能绕过你的验证逻辑。所以在大项目里要禁止第三方代码直接裸调form.submit()统一走封装好的提交入口。5. 让这套验证体系更省心的几条经验5.1 原生校验和自定义 onsubmit 怎么配合HTML5 提供了不要钱的验证能力required、typeemail、minlength、pattern等。这些东西在部分场景下非常好用浏览器会弹出原生气泡提示连 JS 都不用写。但要注意它的副作用原生校验失败时页面根本不会派发submit事件。也就是说如果你既用了required又写了onsubmitreturn check()当字段为空时用户看到的是浏览器原生提示你的自定义check()完全没机会执行。很多新手以为自己的验证函数写错了其实是原生校验先拦住了。我的实践方案是二选一完全自定义表单加novalidate属性关掉原生校验所有验证逻辑自己控制提示风格统一。完全原生不写任何 onsubmit 验证只靠required、pattern、maxlength等属性配合 CSS 的:invalid伪类做样式反馈。最不建议的是两种混着来因为你无法预测到底是原生提示先出现还是你自己的alert先出现体验非常分裂。如果你确实想先手动校验、再调用原生校验的判定可以用form.checkValidity()if (!form.checkValidity()) { return false; }这个方法返回表单是否全部通过原生校验同时会触发浏览器默认的提示 UI。5.2 正则验证的三个常见陷阱正则写不好很容易出现“该拦的没拦不该拦的乱拦”。第一别忘了用锚点^和$。比如你要验证一个数字字段必须全是数字用/\d/去测试abc123因为里面含有数字会返回true。正确写法是/^\d$/从头到尾都要匹配。第二test方法也会被局部状态坑到。当正则表达式带有全局标志/g时同一个正则对象的test()调用会受到上次匹配位置的影响。这是很多人没注意到的坑var reg /^abc$/g; console.log(reg.test(abc)); // true console.log(reg.test(abc)); // false因为 lastIndex 变化了如果你在验证函数里复用了同一个带/g的全局正则对象第二次验证同一内容可能得到相反结果。解决方案正则不要加g标志或者每次匹配前重置lastIndex 0。第三邮箱和手机号的验证规则永远不要写死到“一劳永逸”。邮箱正则/^[^\s][^\s]\.[^\s]$/已经能挡住大部分输入错误但无法保证这个邮箱真的能收到信。手机号更是有国际区号、号码段变化的问题具体取决于业务范围。如果只是国内场景可以用/^1[3-9]\d{9}$/这种比较稳妥的写法有海外用户就不要再这么写了。5.3 前端验证永远只是“第一道门”说句不好听的前端表单验证做得再花哨本质上是提升用户体验不是安全保障。因为请求一旦发送出去服务端拿到的是开发者可控的代码发送的数据任何人通过控制台改一下输入值、或者用脚本直接发请求就能绕过前端验证。我在项目里一直坚持一个底线前端验证尽可能贴近用户服务端验证坚决兜底。前端拦掉 99% 的无意识错误服务端守护最后的 1% 以及所有恶意请求。比如用户名长度、密码强度、重复密码这类规则服务端必须重新校验一遍。这不是前端代码的任务但前端开发者有义务提醒后端同事或者自己顺手把校验逻辑在接口层也实现一遍。5.4 封装一个自己的轻量验证工具项目里表单多了每个页面都写一坨if判断会很烦躁。我习惯用一个非常轻量的规则数组来解决。function validateForm(form, rules) { for (var field in rules) { var value form[field].value.trim(); var checks rules[field]; for (var i 0; i checks.length; i) { if (!checks[i](value)) { alert(field 校验未通过 checks[i].message); return false; } } } return true; }用法var rules { username: [ { check: v v.length 3 v.length 16, message: 用户名需为3~16个字符 } ], email: [ { check: v /^[^\s][^\s]\.[^\s]$/.test(v), message: 邮箱格式不正确 } ], password: [ { check: v v.length 6, message: 密码至少6位 } ] };这样每个表单的验证规则变成数据页面里只写业务规则不再塞满重复的if/else结构。当然这只适合同步验证异步验证还是得回到前面讲的“拦截后手动提交”的模式。最后分享一个我个人的习惯只要是写内联事件属性一律用return 函数名(...)的格式函数内部只返回布尔值不跟preventDefault混用。同时给表单里每个按钮都显式指定type。这两个小习惯看着简单但能帮你省掉大量“验证为什么不生效”的排查时间。表单验证是老话题可越基础的东西越值得抠清楚毕竟大多数产品每天都要跟表单打交道。
返回列表