小议javascript 设计模式 推荐
记得早前就说过要和大家分享“javascript设计模式”,迟迟没写不是因为我懒,最近确实太忙,忙工作,忙旅游(啊哦?),好不容易这几天空闲了,接下来是兑现之前空口白话的时间了。
在讨论设计模式之前,请确认您已经有一定的脚本编程基础,如果不甚了解,建议可以先查阅本人很久之前写的这篇《浅谈javascript面向对象编程》请看下一篇文章。
讲到设计模式,不得不先重点着墨于“接口设计”,因为接口设计在设计模式中的意义太大了,大于模式本身。直观起见,先介绍一下接口定义的形式:
代码如下:
var interface = new Interface("interface",[["getName",1],["getAge",1]]);
可以看出接口函数必须包含两个参数,接口方法定义在一个二维数组中。上例中定义了两个接口方法:getName,getAge,这两个方法都带一个参数,下面我们详细看一下Interface函数的实现代码,从而加深大家对接口的理解。
代码如下:
function Interface(name,methods){
if(arguments.length !=2){
console.log("参数必须为二个");
}
this.name = name;
this.methods = [];
if(methods.length<1){
console.log("第二个参数不能为空数组");
}
for(var i=0;len=methods.length,i<len;i++){
if(typeof methods[i][0] !== 'string'){
console.log("第一个参数数据类型必须为字符串");
}
if(methods[i][1] && typeof methods[i][1] !== 'number'){
console.log("第二个参数数据类型必须为整数型");
}
if(methods[i].length == 1){
methods[i][1] = 0;
}
this.methods.push(methods[i]);
}
}
从代码中不难看出,接口函数的定义规则:[1]Interface函数只能包含两个参数,第一个参数为接口名称,第二个参数是一个二维数组[2]第二个参数不允许为空数组[3]methods参数中的第一个参数必须为字符串类型,用以定义方法名,第二个参数必须为整数类型,用以定义方法的参数个数[4]当methods中方法的参数个数为0时,可以省略不写。
接下来要建一个类,让该类继承前面定义的interface接口,那该怎么做呢,别急,我们需要新增一个方法,见如下代码:
代码如下:
var ioldfish = function(name,age){
this.name = name;
this.age = age;
Interface.regImplement(this,interface);
}
ioldfish.prototype.getName = function(){
alert(this.name);
};
ioldfish.prototype.getAge = function(){
alert(this.age);
};
var fishwl = new ioldfish("老鱼",27);
fishwl.getName();
Interface.regImplement就是我们要新增的方法,作用就是让ioldfish类按照接口interface的规范编码,否则将会在firebug的控制台抛出异常。
看看这个方法的具体实现代码:
代码如下:
Interface.regImplement = function(object){
if(arguments.length<2){
console.log("接口继承参数不能少于二个");
}
for(var i=1;len = arguments.length,i<len;i++){
var interface = arguments[i];
if(interface.constructor !== Interface){
console.log("第三个参数开始必须为接口实例");
}
for(var j=0;len=interface.methods.length,j<len;j++){
var method = interface.methods[j][0];
if(!object[method] || typeof object[method] !=="function" || object[method].getParameters().length !== interface.methods[j][1]){
console.log(""+method+"方法接口不匹配");
}
}
}
}
解读这段代码,你很容易发现:[1]Interface.regImplement继承接口函数的参数至少要有两个,如果有第三个参数,那么该参数必须是Interface接口的实例[2]我们去遍历interface接口中的方法,再与新增类中的方法一一匹配,如果发现继承了该接口规范的类缺少某方法,就会抛出错误提示。[3]接口对于参数个数也进行了匹配,如果接口方法中的参数个数与新增类中方法的个数不匹配也会抛出错误提示。
为了匹配方法中参数个数,这里用到一个getParameters()方法,我们基于Function做个扩展,代码实现如下:
代码如下:
Function.prototype.getParameters = function(){
var str = this.toString();
var paramStr = str.slice(str.indexOf("(")+1,str.indexOf(")")).replace(/\s*/g,'');
try{
return (paramStr.length ==0 ? [] : paramStr.split(","));
}
catch(err){
console.log("非法函数");
}
}
接下来,你可以把所讲的Interface函数,Interface.regImplement函数,还有Function.prototype.getParameters函数整合到一个interface.js的文件中,调试一下新建的这个ioldfish类。看看当类中缺少getAge方法时会怎么样?建议新手,各类情况都模拟一下,加强理解吧!如果你确信已经完全理解接口设计,那就跟着我继续往下走。
Javascript设计模式之单体模式Singleton
单体模式Singleton:这是最基础的设计模式,严格来说没什么模式可言,但是却很容易用也很好用,支付宝很多组件都是通过单体模式设计的。事实上在《浅谈javascript面向对象编程》中阐述原型继承的时候就已经用到了该模式,这里简单带过,重点说一下惰性单体,这对一些不是所有用户都需要,在特定情景下才会用到的组件有非常好的优化作用,他可以让组件的实例化推迟到用户触发他的时候。
代码如下:
var ioldfish = {
name:'老鱼',
age:27,
getName:function(){
alert(name);
},
getAge:function(){
alert(age);
}
}
上例是一个最简单的单体模式,把本人的资料都整合到ioldfish这个对象字面量中,形成一个模块,同时起到了一个命名空间的作用。
代码如下:
var ioldfish =(function(){
var name = '老鱼';
var age = 27;
return{
getName:function(){
alert(name);
},
getAge:function(){
alert(age);
}
}
})();
对第一个单体做简单的修改,通过闭包让name,age成为静态私有变量,确保实例化的时候在内存中始终只有一份,这样更符合单体模式的定义。
下面重点介绍一下惰性单体,废话少说,先看看我们该如何来实现惰性单体:
代码如下:
var ioldfish = (function(){
var uniqueInstance;
var name = '老鱼';
var age = 27;
function constructor(){
return{
getName:function(){
alert(name);
},
getAge:function(){
alert(age);
}
}
}
return{
isInstance:function(){
if(uniqueInstance == null){
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
ioldfish.isInstance().getName();
上面的结构公私分明一目了然,私有变量uniqueInstance(标识类是否已经实例化)和私有方法constructor,返回一个公有方法isInstance(通过该方法可以调用私有方法constructor中定义的方法),形如:ioldfish.isInstance().getName();先通过isInstance()方法判断其是否被实例化,然后通过getName()方法获取到闭包内的私有变量name。该模式的应用场景还是很多的,是不是遇见过页面中需要加载很大的一个日历控件,但并非所有用户都用的到呢?是不是…
Javascript设计模式之工厂模式Factory
工厂模式Factory:先创建一个抽象类,然后基于这个抽象类派生出子类,并在子类中创建工厂方法,从而把实例化推迟到对应的子类中进行,说实话,工厂模式在javascript中的应用有些牵强,毕竟javascript不像java存在硬编码带来的困搅,要学习的只是模式的思想,切忌因为模式而模式。
不妨举个偏激点的例子,为tab切换、下拉列表等组件添加定位,渐隐,延迟等效果,我们可以先为这些组件定义一个接口:
var Iwidget = new Interface("iwidget",[["addEffect"]]);
定义该接口,以便之后派生的子类继承,接口中定义了一个addEffect方法,接口方法实现后,调用的同学大可不必关注各子类中对于addEffect方法的代码实现。
代码如下:
var Widget = function(){};
Widget.prototype={
fire:function(model){
var widget = this.createWidget(model);
//有同学问为什么子类都必须定义接口方法,因为下面要调用嘛
widget.addEffect();
return widget;
},
show:function(){
//show代码具体实现
},
hide:function(){
//hide代码具体实现
},
createWidget:function(model){
alert('抽象类,不可以实例化')
}
};
上例先定义一个抽象类Widget,做为派生子类的父类,由于考虑到这两类组件都涉及到隐藏和显示一个容器,所以在父类中预先定义好show和hide方法以便子类继承。
代码如下:
var xTab = function(){};
extend(xTab,Widget);
xTab.prototype.createWidget = function(model){
var widget;
switch(model){
case 'position':
widget = new xTabPosition();
break;
case 'anim':
widget = new xTabAnim();
break;
case 'delay':
default:
widget = new xTabDelay();
}
};
var dropDown = function(){};
extend(dropDown,Widget);
dropDown.prototype.createWidget = function(model){
var widget;
switch(model){
case 'position':
widget = new dropDownPosition();
break;
case 'anim':
widget = new dropDownAnim();
break;
case 'delay':
default:
widget = new dropDownDelay();
}
};
子类xTab和dropDown继承了父类,并且重写了createWidget方法,不同的子类根据定位,渐隐,延迟效果分别创建不同的实例,只要创建这些实例的类都实现接口中约定的addEffect方法,至于方法代码如何实现,千篇一律,爱咋整咋整。
代码如下:
var xTabPosition = function(){};
xTabPosition.prototype ={
addEffect:function(){
//具体实现代码
}
};
var dropDownPosition = function(){};
dropDownPosition.prototype ={
addEffect:function(){
//具体实现代码
}
};
var dropDownInstance = new dropDown();
dropDownInstance.fire('position');
以此类推,如果您需要为气泡组件添加这些效果,照葫芦画瓢就可以了,说到这里你可以清楚的看到,这种设计模式大大降低了类和类之间的耦合度,而且可以根据具体的交互需求,实现不同的辅助动作,但是也无可避免的增加了代码实现上的复杂性,事实上这种模式并不适合javascript,毕竟它有别于java,不会有类名硬编码的问题,目的是学习他的设计思想,所以以上示例仅供参考,如无大人在旁,小朋友切勿效仿。
对于javascript爱好者来说,更有价值的应该是工厂模式中讲到的的”缓存(memoization)机制”,书上举了个创建XHR对象的例子来说明该特性,但是效果显然不够明显……
memoization名词解释:把函数的每次执行结果都放入一个键值对(数组也可以,视情况而定)中,在接下来的执行中,在键值对中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才 真正执行函数体的求值部分。很明显,找值,尤其是在键值对中找值,比执行函数快多了
在递归调用的时候,memoization的威力才能更好的显现。下面是一个经典的斐波纳契序列,fib(20) 会把fib这个方法执行21891次,如果是fib(40),这会执行331160281次。
代码如下:
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
var iterMemoFib = (function() {
var cache = [1, 1];
var fib = function(n) {
if (n >= cache.length) {
//将一个递归转换成了一个
for (var i = cache.length; i <= n; i++) {
cache[i] = cache[i - 2] + cache[i - 1];
}
}
return cache[n-1];
}
return fib;
})();
将Function的原型扩展memoize 和unmemoize 方法,这样你可以对任何函数实现memoize和解除memoize,当然,这个方法要慎,对一些不是频繁执行的函数,没必要缓存:
代码如下:
Function.prototype.memoize = function() {
var pad = {};
var self = this;
var obj = arguments.length > 0 ? arguments[i] : null;
var memoizedFn = function() {
// 把参数作为数组保存,作为键,把函数执行的结果作为值缓存起来
var args = [];
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
if (!(args in pad)) {
pad[args] = self.apply(obj, arguments);
}
return pad[args];
}
memoizedFn.unmemoize = function() {
return self;
}
return memoizedFn;
}
Function.prototype.unmemoize = function() {
alert("Attempt to unmemoize an unmemoized function.");
return null;
}
使用方法:fib.memoize();
Javascript设计模式之组合模式
组合模式:运用该设计模式可以通过组合对象添加属性和方法,通过递归批量式的让叶子对象得到组合对象的属性和方法。打个比方我们现在要动态创建一个银行列表,按银行类型分为网上银行类,卡通银行类,并可配置他们是否显示。用组合模式如何实现呢?
第一步还是先定义接口,因为要做到某类银行甚至某个银行是否显示可配置,那么我们先约定2个接口,showBank和hideBank。
var IcardItem = new Interface(”icardItem”,[["showBank"],["hideBank"]]);
接下来先定义卡的组合对象,并设置组合对象的基本方法add,remove,getChild,由于这个类继承了IcardItem接口类,所以还定义了showBank,hideBank这两个接口方法。
代码如下:
var cardMain = function(id){
this.cards = [];
this.element = document.createElement("div");
this.element.id = id;
Interface.regImplement(this,IcardItem);
};
cardMain.prototype = {
add:function(card){
this.cards.push(card);
this.element.appendChild(card.getElement());
},
remove:function(card){
for(i=0;len=this.cards.length,i<len;i++){
if(cards[i] == card){
this.cards.splice(i,1);
break;
}
this.element.removeChild(card.getElement());
}
},
getChild:function(i){
return this.cards[i];
},
getElement:function(){
return this.element;
},
showBank:function(){
this.element.style.display ="block";
for(i=0;len=this.cards.length,i<len;i++){
this.cards[i].showBank();
}
},
hideBank:function(){
this.element.style.display ="none";
for(i=0;len=this.cards.length,i<len;i++){
this.cards[i].hideBank();
}
}
};
然后定义叶子对象类bankLogo用以创建银行logo,这里银行logo都以带class的a标签标识:
代码如下:
var bankLogo = function(bankClassName){
this.element = document.createElement("a");
this.element.className = bankClassName;
Interface.regImplement(this,IcardItem);
};
bankLogo.prototype ={
showBank:function(){
this.element.style.display ="block";
},
hideBank:function(){
this.element.style.display ="none";
},
getElement:function(){
return this.element;
}
};
最后设置一个单体对象,将操作银行的相关信息形成一个模块,方便调用:
代码如下:
var BankAction ={
bankList:[],
addBank:function(card){
this.bankList.push(card);
},
innerBank:function(conId){
for(i=0;len=this.bankList.length,i<len;i++){
var cardObj =this.bankList[i].getElement();
}
document.getElementById(conId).appendChild(cardObj);
}
};
到了实现环节了,实例化生成一个包含所有卡的最外层容器,然后根据卡类,分别生成一个放置银行卡和卡通卡的容器,最后生成各银行卡的实例,并按层级关系形成DOM结构:
代码如下:
var bankDivT = new cardMain("PayCard");//创建最外层容器
var ebankCard = new cardMain("ebankCard");//创建网银类银行卡容器
var ktCard = new cardMain("ktCard");//创建卡通类银行卡容器
var ccbBank = new bankLogo('Ebank-CMB');//创建招行银行卡
var abcBank = new bankLogo('Ebank-ABC');//创建农行银行卡
var abcKtBank = new bankLogo('Kt-ABC');//创建卡通农行卡
ebankCard.add(ccbBank);
ebankCard.add(abcBank);
ktCard.add(abcKtBank);
bankDivT.add(ebankCard);
bankDivT.add(ktCard);
BankAction.addBank(bankDivT);
BankAction.innerBank("bankList");
<div id="PayCard">
<div id="ebankCard">
<a class="Ebank-CMB"></a>
<a class="Ebank-ABC"></a>
</div>
<div id="ktCard">
<a class="Kt-ABC"></a>
</div>
</div>
组合模式应用在动态生成用户界面的时候,是非常不错的选择,他可以很大程度的简化粘合性代码,提升维护性。不过还是要慎用,毕竟当叶子对象很多的时候,递归还是存在性能上的问题。
Javascript设计模式之装饰者模式
装饰者模式:可以不创建新的子类,给对象创建新的功能。举例说明:支付宝收银台红包结合余额付款的应用场景。
var Ieconomics = new Interface("ieconomics",[["getPrice"]]);
首先创建一个组件类,基与该组件实例化的对象,会被作为参数传递给装饰者类,以便装饰者调用到该组件中的各方法。
代码如下:
var economic = function(){
Interface.regImplement(this,Ieconomics);
};
economic.prototype={
getPrice:function(){
//代码实现
}
};
然后创建一个装饰者抽象类,作为派生装饰者选件类的父类:
代码如下:
var economicsDecorator = function(economic){
this.economic = economic;
this.regImplement(economic,Ieconomics);
};
economicsDecorator.prototype={
getPrice:function(){
return this.economic.getPrice();
}
};
最后基于上面这个抽象类,派生出一个装饰者选件类:
代码如下:
//红包装饰者选件类
var coupon = function(economic){
//调用装饰着抽象类的构造函数
economicsDecorator.call(this,economic);
};
extend(coupon,couponDecorator);
coupon.prototype=function(){
//改写getPrice方法
getPrice:function(){
return this.economic.getPrice() - this.getCoupon();
},
getCoupon:function(){
//获取红包总价具体实现
}
};
var myCoupon = new economic();
myCoupon = new coupon(myCoupon);
实现装饰者模式就是这么简单,首先创建一个组件的实例myCoupon,然后将该对象作为参数传递给装饰者选件类coupon。你会发现两句代码中我都把值赋给了变量myCoupon,这是因为他们都实现了同一个接口类,他们之间是可以互换使用的。
看到这里心细的同学可能会发现,我们在coupon类中新增了一个getCoupon方法,目前来看不会有任何问题,但是如果我们继续创建一个购物卷装饰者选件类,然后结合红包一起用呢?
代码如下:
//购物卷装饰者选件类
var voucher = function(economic){
economicsDecorator.call(this,economic);
};
extend(voucher,couponDecorator);
voucher.prototype=function(){
getPrice:function(){
return this.getPrice() - this.getVoucher();
},
getVoucher:function(){
//获取优惠卷总价具体实现
}
};
var myCoupon = new economic();
myCoupon = new coupon(myCoupon);
myCoupon = new voucher(myCoupon);
在这种场景下面getCoupon方法已经找不到了,这是因为voucher装饰myCoupon的时候,它的父类economicsDecorator不包含getCoupon方法,那自然是取不到的了,那该怎么办呢?
分析一下装饰者抽象类economicsDecorator,我们传递了一个myCoupon的引用作为参数,我们可以通过这个参数做一些小动作,获得新增加的方法。
代码如下:
var economicsDecorator = function(economic){
this.economic = economic;
this.interface = Ieconomics;
for(var k in this.economic){
if(typeof this.economic[key] !== "function"){
continue;
var i;
for(i = 0;len = this.interface.methods.length,i < len; i++) {
//通过遍历比较在接口类中是否包含此方法,如果包含返回下一个
if(key == this.interface.methods[i][0]) {
break;
}
}
if(i < this.interface.methods.length)
continue;
var decorator = this;
//采用匿名函数调用方式来定义新方法
(function(methodName) {
decorator[methodName] = function() {
return decorator.economic[methodName]();
};
})(key);
}
}
}
this.regImplement(economic,Ieconomics);
};
economicsDecorator.prototype={
getPrice:function(){
return this.economic.getPrice();
}
};
看上面的代码,我们对装饰者抽象类做了一些修改,这样做是为了确保在装饰者选件类中一旦定义新方法,可以在装饰者抽象类中动态的定义出来。这里只是提供一个使用装饰者模式的思路,具体的实现代码远比这个复杂,由于项目还在开发中,demo暂不提供,支付宝新版收银台发布后,会跟大家再做个详细的设计分享。
Javascript设计模式之桥接模式
桥接模式:将抽象和其实现分离开来,以便二者独立变化。其实很简单,只是在API和具体事件之间增加一个桥梁,从而降低API和使用他的类和对象之间的耦合。
事实上对大多数同学来说桥接模式并不陌生,下面的this.getName就是一种桥接方法,他是外访问的一个接口,他的内部实现是通过访问内部私有变量来实现的,这个方法起到了外部和内部沟通的桥梁作用。
代码如下:
var ioldfish = function(){
var name = '老鱼';
this.getName = function(){
alert(name);
}
}
桥接模式用的最多的还是在事件监听器回调函数。下面这个是获取用户信息的API接口函数:
代码如下:
function getUserInfo(userid,callback){
asyncRequest('GET','userInfo?userid='+userid,function(resp){
callback(resp.responseText);
});
}
接下去我们要做的是把这个API和某个事件的触发建立一个桥梁关系
addEvent(element,'click',bridgeMethod);
function bridgeMethod(e){
getUserInfo(this.userid,function(){
//回调函数实现代码
});
}
这里在element对象click的时候触发函数并不是getIserInfo,而是新建了一个桥接方法bridgeMethod,通过这层桥接使得API接口函数和click事件相对独立,这样大大拓宽了API的适用范围。
Javascript设计模式之适配器模式
适配器模式:打个比方,你维护了一个系统,之前一直都是用prototype框架,但是现在打算新引入YUI框架,那如何让两个框架平稳过度呢
,举个例子,如何将prototype中的$方法转换为YUI中的get方法:
代码如下:
function $(){};
function YAHOO.util.Dom.get=function(el){};
function prototypeToYuiAdapter(){
return YAHOO.util.Dom.get(arguments);
}
你要在prototype中使用yui的get方法,只需要做以下申明即可:
$ = prototypeToYuiAdapter;
这样的话,在prototype中就可以使用YUI中的get方法了。本人并不是很推崇这种模式,所以不多做阐述,事实上我觉得不到万不得以,我们根本不需要使用这种模式,作为一名负责任的设计者,我宁可做代码重构也不希望使用该模式,只能作为无奈之下的过渡型方案使用。
Javascript设计模式之门面模式,观察者模式
门面模式:这应该是所有脚本框架中都用到的,最基础的设计模式,随便找个框架中定义好的方法看看就行了,比如说YUI中的setStyle方法等等等等。在这里就不多阐述了。
观察者模式:该设计模式应用在javascript上似乎更为牵强,不甚理解,这里就不说了,以免误人子第,如有心得者不吝赐教。
一天时间都耗在这篇博文上了,好象还有很多想写的,看来要把心里想的东西写清楚还是不容易的,再整理整理再说吧,敬请期待!