本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
大家接触到 web 之后,一定会遇到一个硬骨头— IE 浏览器。当年我的一位学长告诉过我, IE 不死,社会将无法进步,那时年少无知,不理解其中的含义,现在时过境迁,回头想想,路人心在看,却是局中人~。
跑题了跑题了,聊回我们今天的主题,大家肯定都会遇到过,当写好一部分功能之后,调试好了,有时可能已经部署到服务器上了,这时候项目经理拍了拍你的肩膀,和蔼可亲的对你说,来了个小需求,改动不大,你给加一下。
这个时候,你要仔细斟酌一下这个“ 改动不大 ”的意思,一般来说,是指的结果改动不大,那具体实现过程改动大不大,就内部消化一下吧。
为了减少上面那种情况,我们今天一起研究个新设计模式,访问者模式。
访问者模式
针对对象结构中的元素,定义在不改变对象的前提下访问结构中元素的新方法 。
举个例子,在 IE 中,用 attachEvent 绑定事件的时候, this 指向的是 window 而不是当前这个元素,那我们在使用 this 的时候,就会报错:
var bindEvent = function(dom, type, fn) {
if(dom.addEventListener) {
dom.addEventListener(type, fn, false);
} else if(dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else{
dom['on' + type] = fn;
}
}
var demo = document.getElementById('demo');
bindEvent(demo. 'click', function() {
this.style.background = 'red'; //IE中会报this.style为空或不为对象
});
现在修改方法有两种,一种是把所有的 this 替换成 demo ,这种方法显然会修改方法内部参数,后续修改维护可能成本更大,那还有一种方法就是,给操作元素增加新的操作方法,如下:
function bindIEEvent(dom, type, fn, data) {
var data = data || {
};
dom.attachEvent('on' + type, function(e) {
fn.call(dom, e, data);
});
};
核心方法是调用了一次 call 方法,我们知道 call 和 apply 是更改函数执行时的作用域,这正是访问者模式的精髓,通过这个方法,我们就可以让某个对象在其他作用域中运行。
还有一个有点是, call 和 apply 允许我们添加额外的参数,这一点很重要,因为有时候只通过事件参数 e 获取到的数据不够。
function $(id) {
return document.getElementById(id)};
bindIEEvent($('btn'), 'click', function(e, d) {
$('test').innerHTML = e.type + d.text + this.tagName;
}, {
text: 'test demo'});
上面代码,点击一下 id 为 btn 的按钮, id 为 test 的段落内容会变为 click test demo BUTTON 。
访问者模式的应用不仅如此, JS 原生对象构造器就设计成一个访问者,比如在判断数据类型的时候,我们通过 Object.prototype.toString.call 的方式。这里我们可以扩展一下,在我们为对象添加属性的时候通常都是没有顺序的,这样就很难找到最后一次添加的属性,如果能像处理数组一样处理对象,就好了,我们通过 push 添加数据,通过 pop 删除最后一个成员。
这里我们需要创建一个访问器,将必要的方法封装在里面,方便使用。
//访问器
var Vistor = (function() {
return {
//截取方法
splice: function() {
//splice 方法参数,从原参数的第二个参数开始算起
var args = Array.prototype.splice.call(arguments, 1);
//对第一个参数对象执行 splice 方法
return Array.prototype.splice.apply(arguments[0], args);
},
//追加数据方法
push: function() {
//强化数组对象,使它拥有 length 属性
var len = arguments[0].length || 0;
//添加的数据从原参数的第二个参数算起
var args = this.splice(arguments, 1);
//校正 length 属性
arguments[0].length = len + arguments.length - 1;
//对第一个参数对象执行 push 方法
return Array.prototype.push.apply(arguments[0], args);
},
//弹出最后一次添加的元素
pop: function() {
//对第一个参数对象执行 pop 方法
return Array.prototype.pop.apply(arguments[0]);
}
}
})();
有了这个访问器,我们就可以操作数组对象了。
var a = new Object();
console.log(a.length); //undefined
Visitor.push(a, 1,2,3,4);
console.log(a.length); //4
Visitor.push(a, 4,5,6);
console.log(a); //Object {0: 1, 1: 2, 2: 3, 3: 4, 4: 4, 5: 5, 6: 6, lenght: 7}
Visitor.splice(a, 2);
console.log(a); //Obkect {0: 1, 1: 2, length: 2}
访问者模式可以解决数据与数据操作方法之间的耦合,将数据的操作方法独立于数据,使其可以自由演变,因此访问者模式适用于那些数据稳定,但是操作方法易变的化境。
中介者模式
通过中介对象封装一些列对象之间的交互,使对象之间不再相互引用,降低他们之间的耦合 。
中介模式与我们之前提到的观察者模式有点类似,它们都是通过消息的收发机制实现的,有点不同的是,观察者模式中,一个对象既可以是消息的发送者,也可以是接收者,他们之间交流信息依托于消息系统实现的解耦。
而中介者模式中,消息发送只能通过中介对象来完成,对象不能订阅消息,只有那些活跃对象(订阅者),才可订阅中介者的消息,而且观察者模式还需要写一个消息系统,那样会增加开发成本。下面我们写一个中介者对象:
//中介者对象
var Mediator = function() {
//消息对象
var _msg = {
};
return {
/**
* 订阅消息方法
* 参数 type 消息名称
* 参数 action 消息回调函数
*/
register: function(type, action) {
//如果该消息存在
if(_msg[type]) {
//存入回调函数
msg[type].push(action);
} else {
//不存在则建立消息容器
_msg[type] = [];
//存入新消息回调函数
_msg[type].push(action);
}
},
/**
* 发布消息方法
* 参数 type 消息名称
*/
send: function(type) {
//如果该消息已经被订阅
if(_msg[type]) {
//遍历已经存储的消息回调函数
for(var i = 0, len = _msg[type].length; i < len; i++) {
//执行该回调函数
_msg[type][i] && _msg[type][i]();
}
}
}
}
} ();
中介者创建出来了,为了保险起见,先用测试用例试验一下。
//单元测试
//订阅demo消息,执行回调函数--输出 first
Mediator.register('demo', function() {
console.log('first');
})
//订阅demo消息,执行回调函数--输出 second
Mediator.register('demo', function() {
console.log('second');
})
//发布 demo 消息
Mediator.send('demo');
//输出结果以此为
//first
//second
熟悉外观模式的同学可能觉得这个和外观模式也有点像,中介者模式会对多个对象交互的封装,且这些对象一般处于同一层面上,并且封装的交互在中介者内部,而外观模式的目的是为了提供更简单易用的接口。
本节的两个设计模式还是比较易懂的,大家使用设计模式的时候,可能会越用越乱,感觉这个和之前接触的别的设计模式有点像,但是又忘记具体是哪一个,总是有种狗熊掰棒子的感觉,就会现在这一个。
其实这种感觉和正常,知识本来就有相似性,感觉乱说明你懂得多,这个时候就是一个量变转化质变的过程,各位同学坚持住,多看几遍,多用几遍,下一遍就能醍醐灌顶。