利用策略模式与装饰模式重构JavaScript表单验证

作者:闭包旅人时间:2026-10-21 22:01:45浏览:174

本文深入解析如何利用JavaScript中的策略模式与装饰模式重构表单验证逻辑。通过AOP前置装饰解决函数职责耦合问题,利用策略对象管理校验规则以提升扩展性,并结合工厂模式处理多表单场景。文中还详细剖析了闭包、this劫持及数组引用等常见陷阱,提供完整代码与调试经验。

简单表单验证实现

基础表单校验

HTML布局


<!-- validata.html -->
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Validata</title>
</head>
<body>
 <form id="form">
 <label for="username">账号:</label><input id="username" type="text"><br>
 <label for="password">密码:</label><input id="password" type="password"><br>
 <label for="phonenum">手机:</label><input id="phonenum" type="text"><br>
 <input id="submit" type="button" value="提交">
 </form>
 <p id="warn"></p>
 <script src="validata.js"></script>
</body>
</html>

简单表单验证实现

首先实现基础功能

随后引入设计模式完善


// validata.js
var form = document.getElementById('form'),
 warn = document.getElementById('warn');
var validata = function(){
 if(form.username.value === ''){
 return warn.textContent = '账号不能为空';
 }
 if(form.password.value === ''){
 return warn.textContent = '密码不能为空';
 }
 if(form.phonenum.value === ''){
 return warn.textContent = '手机号不能为空';
 }
 var msg = {
 username: form.username.value,
 password: form.password.value,
 phonenum: form.phonenum.value
 }
 //ajax('...', msg); ajax提交数据略
 return warn.textContent = '用户信息已成功提交至服务器';
}
form.submit.onclick = function(){
 validata();
}

现有代码问题分析

接着分析现有代码

validata函数缺乏复用性,且存在两个缺陷

  • 函数混合了验证与提交逻辑,违反单一职责原则
  • 验证扩展性不足,添加规则需修改源码,违反开放-封闭原则

因此需进行优化

使用装饰模式重构

装饰模式重构

利用装饰模式解决函数职责过多问题

实现方式简洁

优化AOP前置装饰函数(Function.prototype.before)

若扩展函数(beforeFn)返回false,则中断当前函数执行

将表单验证函数设为提交函数的前置装饰器

提交前执行验证,验证失败则阻止数据提交


var form = document.getElementById('form'),
 warn = document.getElementById('warn');
Function.prototype.before = function(beforeFn){
 var self = this;
 return function(){
 if(beforeFn.apply(this, arguments) === false)
  return;
 return self.apply(this, arguments);
 }
}//改进的AOP前置装饰函数
var validata = function(){
 if(form.username.value === ''){
 warn.textContent = '账号不能为空';
 return false;
 }
 if(form.password.value === ''){
 warn.textContent = '密码不能为空';
 return false;
 }
 if(form.phonenum.value === ''){
 warn.textContent = '手机号不能为空';
 return false;
 }
}
var submitMsg = function(){ //将提交的功能从validata函数中提取出来
 var msg = {
 username: form.username.value,
 password: form.password.value,
 phonenum: form.phonenum.value
 }
 //ajax('...', msg);
 return warn.textContent = '用户信息已成功提交至服务器';
}
submitMsg = submitMsg.before(validata);
//让表单验证函数成为表单提交函数的装饰者
form.submit.onclick = function(){
 submitMsg();
};

使用策略模式重构

策略模式重构

解决函数灵活性不足的问题

策略模式需包含策略对象/类及环境对象/类

策略对象应封装校验规则

考虑到页面可能存在多个验证表单

建议采用工厂类模式

完整代码示例如下


var form = document.getElementById('form'),
 warn = document.getElementById('warn');
Function.prototype.before = function(beforeFn){
 var self = this;
 return function(){
 if(beforeFn.apply(this, arguments) === false)
  return;
 return self.apply(this, arguments);
 }
}
var vldStrategy = { //策略对象-验证规则
 isNonEmpty: function(value, warnMsg){ //输入不为空
 if(value === '')
  return warnMsg;
 },
 isLongEnough: function(value, length, warnMsg){ //输入足够长
 if(value.length < length)
  return warnMsg;
 },
 isShortEnough: function(value, length, warnMsg){ //输入足够短
 if(value.length > length)
  return warnMsg;
 },
 isMobile: function(value, warnMsg){ //手机号验证
 var reg = /^1[3|5|8][0-9]{9}$/;
 if(!reg.test(value))
  return warnMsg;
 }
}
var Validator = function(){ //环境类
 this.rules = []; //数组用于存放负责验证的函数
};
Validator.prototype.add = function(domNode, ruleArr){ //添加验证规则
 var self = this;
 for(var i = 0, rule; rule = ruleArr[i++];){
 (function(rule){
  var strategyArr = rule.strategy.split(':'),
   warnMsg = rule.warnMsg;
  self.rules.push(function(){
  var tempArr = strategyArr.concat();
  var ruleName = tempArr.shift();
  tempArr.unshift(domNode.value);
  tempArr.push(warnMsg);
  return vldStrategy[ruleName].apply(domNode, tempArr);
  });
 })(rule);
 }
 return this;
};
Validator.prototype.start = function(){ //开始验证表单
 for(var i = 0, vldFn; vldFn = this.rules[i++];){
 var warnMsg = vldFn();
 if(warnMsg){
  warn.textContent = warnMsg;
  return false;
 }
 }
}
var vld = new Validator();
vld.add(form.username, [
 {
 strategy: 'isNonEmpty',
 warnMsg: '账号不能为空'
 },
 {
 strategy: 'isLongEnough:4',
 warnMsg: '账号不能小于4位'
 },
 {
 strategy: 'isShortEnough:20',
 warnMsg: '账号不能大于20位'
 }
]).add(form.password, [
 {
 strategy: 'isNonEmpty',
 warnMsg: '密码不能为空'
 }
]).add(form.phonenum, [
 {
 strategy: 'isNonEmpty',
 warnMsg: '手机号不能为空'
 },
 {
 strategy: 'isMobile',
 warnMsg: '手机号格式不正确'
 }
]);
var submitMsg = function(){
 var msg = {
 username: form.username.value,
 password: form.password.value,
 phonenum: form.phonenum.value
 }
 //ajax('...', msg);
 return warn.textContent = '用户信息已成功提交至服务器';
}
submitMsg = submitMsg.before(vld.start.bind(vld));
form.submit.onclick = function(){
 submitMsg();
};
//这里只是模拟提交,实际应该用form.onsubmit

简单表单验证实现

简单表单验证实现

简单表单验证实现

常见错误与调试分析

问题排查

总结常见错误及开发中遇到的问题


Validator.prototype.add = function(domNode, ruleArr){
 var self = this;
 for(var i = 0, rule; rule = ruleArr[i++];){
 (function(rule){
  var strategyArr = rule.strategy.split(':'),
   warnMsg = rule.warnMsg;
  self.rules.push(function(){
  var tempArr = strategyArr.concat();
  var ruleName = tempArr.shift();
  tempArr.unshift(domNode.value);
  tempArr.push(warnMsg);
  return vldStrategy[ruleName].apply(domNode, tempArr);
  });
 })(rule);
 }
 return this;
};

Validator原型链上的add函数需注意几点

首先使用IIFE立即执行函数解决闭包问题,此处不再赘述

函数嵌套导致this指向被劫持,必须缓存this引用

var self = this;

起初未拷贝数组而直接引用strategyArr


Validator.prototype.add = function(domNode, ruleArr){ //添加验证规则
 var self = this;
 for(var i = 0, rule; rule = ruleArr[i++];){
 (function(rule){
  var strategyArr = rule.strategy.split(':'),
   warnMsg = rule.warnMsg;
  self.rules.push(function(){
  // var tempArr = strategyArr.concat();
  var ruleName = strategyArr.shift();
  strategyArr.unshift(domNode.value);
  strategyArr.push(warnMsg);
  return vldStrategy[ruleName].apply(domNode, strategyArr);
  });
 })(rule);
 }
 return this;
};

首次提交正常,二次提交报错

简单表单验证实现

因首次提交后闭包内strategyArr状态已变更

后续提交对该数组操作将产生非预期结果

此处疏忽导致断点调试耗时较长 __冏rz


Validator.prototype.start = function(){ //开始验证表单
 for(var i = 0, vldFn; vldFn = this.rules[i++];){
 var warnMsg = vldFn();
 if(warnMsg){
  warn.textContent = warnMsg;
  return false;
 }
 }
}

修正前的错误代码如下


Validator.prototype.start = function(){ //开始验证表单
 for(var i = 0, vldFn; vldFn = this.rules[i++];){
 var warnMsg = vldFn();
 if(warnMsg)
  warn.textContent = warnMsg;
  return false;
 }
}

仅因遗漏一层大括号

原仅一行,添加return false时遗漏大括号

此处为求简洁省略大括号

日常编码严禁此写法,易埋隐患


submitMsg = submitMsg.before(vld.start.bind(vld));

添加装饰者环节亦需留意

未使用bind将导致this劫持并报错

以上为利用策略模式与装饰模式扩展JavaScript表单验证功能的介绍,希望对各位有所帮助,如有任何疑问请留言,小编会及时回复。在此非常感谢大家对本站网站的支持!

作者最新文章