使用装饰者重构表单验证(插件式的表单验证)

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。

在一个web项目中,可能存在非常多的表单,如注册、登录、修改用户信息等。在表单数据提交给后台之前,常常要做一些校验,比如登录的时候需要验证用户名和密码是否为空,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<body>
用户名:<input id="username" type="text"/><br/>
密码:<input id="password" type="password" /> <br/>
<input id="submitBtn" type="button" value="提交"/>
</body>
<script>
var username = document.getElementById('username'),
password = document.getElementById('password'),
submitBtn = document.getElementById('submitBtn');
var formSubmit = function(){
if(username.value === ''){
return alert('用户名不能为空');
}
if(password.value === ''){
return alert('密码不能为空');
}
var param = {
username:username.value,
password:password.value
}
ajax('http:xxx.com',param); //ajax具体实现略
}
submitBtn.onclick = function(){
formSubmit();
}
</script>

formSubmit函数在此处承担了两个职责,除了提交ajax请求之外,还要验证用户输入的合法性。这种代码一般会造成函数臃肿,职责混乱,二来谈不上任何可复用性。
本节的目的是分离校验输入和提交ajax请求的代码,我们把校验输入的逻辑放到validata函数中,并且约定当validata函数返回false的时候,表示校验未通过,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var username = document.getElementById('username'),
password = document.getElementById('password'),
submitBtn = document.getElementById('submitBtn');
var validata = function(){
if(username.value === ''){
return alert('用户名不能为空');
}
if(password.value === ''){
return alert('密码不能为空');
}
}
var formSubmit = function(){
if(validata() === false){
return;
}
var param = {
username:username.value,
password:password.value
}
ajax('http:xxx.com',param); //ajax具体实现略
}
submitBtn.onclick = function(){
formSubmit();
}

现在的代码已经有了一些改进,我们把校验的逻辑都放到了validata函数中,但formSubmit函数的内部还要计算validata函数的返回值,因为返回值的结果表明了是否通过校验。

接下来进一步优化这段代码,使validata和formSubmit完全分离开来。首先改写Function.prototype.before,如果beforefn的执行结果返回false,表示不再执行后面的原函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var username = document.getElementById('username'),
password = document.getElementById('password'),
submitBtn = document.getElementById('submitBtn');
Function.prototype.before = function(beforefn){
var _self = this;
return function(){
if(beforefn.apply(this,arguments) === false){
//beforefn返回false的情况下直接return,不再执行后面的原函数
return;
}
return _self.apply(this,arguments);
}
}
var validata = function(){
if(username.value === ''){
return alert('用户名不能为空');
}
if(password.value === ''){
return alert('密码不能为空');
}
}
var formSubmit = function(){
var param = {
username:username.value,
password:password.value
}
ajax('http:xxx.com',param); //ajax具体实现略
}
formSubmit = formSubmit.before(validata);
submitBtn.onclick = function(){
formSubmit()
}

在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系,validata成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分开维护这两个函数,再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式,用在不同的项目当中。


值得注意的是,因为函数通过Function.prototype.before或者Function.prototype.after被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。代码如下:

1
2
3
4
5
6
7
8
9
10
var fun = function(){
alert(1);
}
func.a = "a";
func = func.after.after(function(){
alert(2);
});
alert(func.a); //输出:undefined

另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。


本文选自:
《javascript设计模式与开发实践》