Javascript十大常用设计模式
一、工厂模式(解决多个相似的问题)
工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。(工厂模式是为了解决多个类似对象声明的问题;也就是为了解决实列化对象产生重复的问题。)
- 优点:能解决多个相似的问题。
- 缺点:不能知道对象识别的问题(对象的类型不知道)。
- 复杂的工厂模式定义是:将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。
// 定义自行车的构造函数
var BicycleShop = function (name) {
this.name = name;
this.method = function () {
return this.name;
}
};
BicycleShop.prototype = {
constructor: BicycleShop,
/*
* 买自行车这个方法
* @param {model} 自行车型号
*/
sellBicycle: function (model) {
var bicycle = this.createBicycle(model);
// 执行A业务逻辑
bicycle.A();
// 执行B业务逻辑
bicycle.B();
return bicycle;
},
createBicycle: function (model) {
throw new Error("父类是抽象类不能直接调用,需要子类重写该方法");
}
};
// 实现原型继承
function extend(Sub, Sup) {
//Sub表示子类,Sup表示超类
// 首先定义一个空函数
var F = function () {
};
// 设置空函数的原型为超类的原型
F.prototype = Sup.prototype;
// 实例化空函数,并把超类原型引用传递给子类
Sub.prototype = new F();
// 重置子类原型的构造器为子类自身
Sub.prototype.constructor = Sub;
// 在子类中保存超类的原型,避免子类与超类耦合
Sub.sup = Sup.prototype;
if (Sup.prototype.constructor === Object.prototype.constructor) {
// 检测超类原型的构造器是否为原型自身
Sup.prototype.constructor = Sup;
}
}
var BicycleChild = function (name) {
this.name = name;
// 继承构造函数父类中的属性和方法
BicycleShop.call(this, name);
};
// 子类继承父类原型方法
extend(BicycleChild, BicycleShop);
// BicycleChild 子类重写父类的方法
BicycleChild.prototype.createBicycle = function () {
var A = function () {
console.log("执行A业务操作");
};
var B = function () {
console.log("执行B业务操作");
};
return {
A: A,
B: B
}
}
var childClass = new BicycleChild("demo");
console.log(childClass);
上面只是"
demo
"自行车这么一个型号的,如果需要生成其他型号的自行车的话,可以编写其他子类,工厂模式最重要的优点是:可以实现一些相同的方法,这些相同的方法我们可以放在父类中编写代码,那么需要实现具体的业务逻辑,那么可以放在子类中重写该父类的方法,去实现自己的业务逻辑;使用专业术语来讲的话有 2点:第一:弱化对象间的耦合,防止代码的重复。在一个方法中进行类的实例化,可以消除重复性的代码。第二:重复性的代码可以放在父类去编写,子类继承于父类的所有成员属性和方法,子类只专注于实现自己的业务逻辑。
二、单体模式(实例化一次>>弹窗)
单体模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一变量进行访问。
什么是单体模式?单体模式是一个用来划分命名空间并将一批属性和方法组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。
- 优点:
- 可以用来划分命名空间,减少全局变量的数量。
- 使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护。
- 可以被实例化,且实例化一次。
使用单体模式创建元素
// 创建div
var createWindow = function () {
var div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
// 创建iframe
var createIframe = function () {
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
return iframe;
};
// 获取实例的封装代码
var getInstance = function (fn) {
var result;
return function () {
return result || (result = fn.call(this, arguments));
}
};
// 测试创建div
var createSingleDiv = getInstance(createWindow);
document.getElementById("Id").onclick = function () {
var win = createSingleDiv();
win.style.display = "block";
};
// 测试创建iframe
var createSingleIframe = getInstance(createIframe);
document.getElementById("Id").onclick = function () {
var win = createSingleIframe();
win.src = "http://cnblogs.com";
};
如上代码:我们使用一个参数fn传递进去,如果有result这个实例的话,直接返回,否则的话,当前的getInstance函数调用fn这个函数,是this指针指向与这个fn这个函数;之后返回被保存在result里面;现在我们可以传递一个函数进去,不管他是创建div也好,还是创建iframe也好,总之如果是这种的话,都可以使用getInstance来获取他们的实例对象;
三、模块模式(返回对象的匿名函数)
通过单体模式理解了是以对象字面量的方式来创建单体模式的
模块模式的思路是为单体模式添加私有变量和私有方法能够减少全局变量的使用
模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,先定义了私有变量和函数,供内部函数使用,然后将一个对象字面量作为函数的值返回,返回的对象字面量中只包含可以公开的属性和方法。这样的话,可以提供外部使用该方法;由于该返回对象中的公有方法是在匿名函数内部定义的,因此它可以访问内部的私有变量和函数。
function CustomType() {
this.name = "tugenhua";
};
CustomType.prototype.getName = function(){
return this.name;
}
var application = (function(){
// 定义私有
var privateA = "aa";
// 定义私有函数
function A(){};
// 实例化一个对象后,返回该实例,然后为该实例增加一些公有属性和方法
var object = new CustomType();
// 添加公有属性
object.A = "aa";
// 添加公有方法
object.B = function(){
return privateA;
}
// 返回该对象
return object;
})();
下面我们来打印下application该对象;如下:
console.log(application);
继续打印该公有属性和方法如下:
console.log(application.A);// aa
console.log(application.B()); // aa
console.log(application.name); // tugenhua
console.log(application.getName());// tugenhua
增强的模块模式的使用场合是:适合那些单列必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增强的情况。
四、代理模式(代替本体被实例化>可推迟 做缓存)
代理是一个对象,它可以用来控制对本体对象的访问,它与本体对象实现了同样的接口,代理对象会把所有的调用方法传递给本体对象的;代理模式最基本的形式是对访问进行控制,而本体对象则负责执行所分派的那个对象的函数或者类,简单的来讲本地对象注重的去执行页面上的代码,代理则控制本地对象何时被实例化,何时被使用;我们在上面的单体模式中使用过一些代理模式,就是使用代理模式实现单体模式的实例化,其他的事情就交给本体对象去处理;
- 优点:
- 代理对象可以代替本体被实例化,并使其可以被远程访问;
- 它还可以把本体实例化推迟到真正需要的时候;对于实例化比较费时的本体对象,或者因为尺寸比较大以至于不用时不适于保存在内存中的本体,我们可以推迟实例化该对象;
使用代理模式来编写预加载图片的代码如下:
ar myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
// 代理模式
var ProxyImage = (function(){
var img = new Image();
img.onload = function(){
myImage.setSrc(this.src);
};
return {
setSrc: function(src) {
myImage.setSrc("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
img.src = src;
}
}
})();
// 调用方式
ProxyImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");
myImage 函数只负责创建img元素,代理函数ProxyImage 负责给图片设置loading图片,当图片真正加载完后的话,调用myImage中的myImage.setSrc方法设置图片的路径
-------------------------------------------------------------------------------------
第二种方案使用代理模式,其中myImage 函数只负责做一件事,创建img元素加入到页面中,其中的加载loading图片交给代理函数ProxyImage 去做,当图片加载成功后,代理函数ProxyImage 会通知及执行myImage 函数的方法,同时当以后不需要代理对象的话,我们直接可以调用本体对象的方法即可;
理解缓存代理:
// 计算乘法
var mult = function () {
var a = 1;
for (var i = 0, ilen = arguments.length; i < ilen; i += 1) {
a = a * arguments[i];
}
return a;
};
// 计算加法
var plus = function () {
var a = 0;
for (var i = 0, ilen = arguments.length; i < ilen; i += 1) {
a += arguments[i];
}
return a;
}
// 代理函数
var proxyFunc = function (fn) {
var cache = {}; // 缓存对象
return function () {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args]; // 使用缓存代理
}
return cache[args] = fn.apply(this, arguments);
}
};
var proxyMult = proxyFunc(mult);
console.log(proxyMult(1, 2, 3, 4)); // 24
console.log(proxyMult(1, 2, 3, 4)); // 缓存取 24
var proxyPlus = proxyFunc(plus);
console.log(proxyPlus(1, 2, 3, 4)); // 10
console.log(proxyPlus(1, 2, 3, 4)); // 缓存取 10
缓存代理的含义就是对第一次运行时候进行缓存,当再一次运行相同的时候,直接从缓存里面取,这样做的好处是避免重复一次运算功能,如果运算非常复杂的话,对性能很耗费,那么使用缓存对象可以提高性能;
五、职责链模式
职责连是由多个不同的对象组成的,发送者是发送请求的对象,而接收者则是链中那些接收这种请求并且对其进行处理或传递的对象。请求本身有时候也可以是一个对象,它封装了和操作有关的所有数据
- 优点:
- 解耦了请求发送者和N个接收者之间的复杂关系,不需要知道链中那个节点能处理你的请求,所以你 只需要把请求传递到第一个节点即可。
- 链中的节点对象可以灵活地拆分重组,增加或删除一个节点,或者改变节点的位置都是很简单的事情。
- 我们还可以手动指定节点的起始位置,并不是说非得要从其实节点开始传递的.
- 缺点:职责链模式中多了一点节点对象,可能在某一次请求过程中,大部分节点没有起到实质性作用,他们的作用只是让请求传递下去,从性能方面考虑,避免过长的职责链提高性能。
基本实现流程如下:
-
发送者知道链中的第一个接收者,它向这个接收者发送该请求。
-
每一个接收者都对请求进行分析,然后要么处理它,要么它往下传递。
-
每一个接收者知道其他的对象只有一个,即它在链中的下家(successor)。
-
如果没有任何接收者处理请求,那么请求会从链中离开。
下面列举个列子来说明职责链的好处:
天猫每年双11都会做抽奖活动的,比如阿里巴巴想提高大家使用支付宝来支付的话,每一位用户充值500元到支付宝的话,那么可以100%中奖100元红包,
充值200元到支付宝的话,那么可以100%中奖20元的红包,当然如果不充值的话,也可以抽奖,但是概率非常低,基本上是抽不到的,当然也有可能抽到的。
我们下面可以分析下代码中的几个字段值需要来判断:
-
orderType(充值类型),如果值为1的话,说明是充值500元的用户,如果为2的话,说明是充值200元的用户,如果是3的话,说明是没有充值的用户。
-
isPay(是否已经成功充值了): 如果该值为true的话,说明已经成功充值了,否则的话 说明没有充值成功;就当作普通用户来购买。
-
count(表示数量);普通用户抽奖,如果数量有的话,就可以拿到优惠卷,否则的话,不能拿到优惠卷。
function order500(orderType,isPay,count){
if(orderType == 1 && isPay == true) {
console.log("亲爱的用户,您中奖了100元红包了");
}else {
//我不知道下一个节点是谁,反正把请求往后面传递
return "nextSuccessor";
}
};
function order200(orderType,isPay,count) {
if(orderType == 2 && isPay == true) {
console.log("亲爱的用户,您中奖了20元红包了");
}else {
//我不知道下一个节点是谁,反正把请求往后面传递
return "nextSuccessor";
}
};
function orderNormal(orderType,isPay,count){
// 普通用户来处理中奖信息
if(count > 0) {
console.log("亲爱的用户,您已抽到10元优惠卷");
}else {
console.log("亲爱的用户,请再接再厉哦");
}
}
// 下面需要编写职责链模式的封装构造函数方法
var Chain = function(fn){
this.fn = fn;
this.successor = null;
};
Chain.prototype.setNextSuccessor = function(successor){
return this.successor = successor;
}
// 把请求往下传递
Chain.prototype.passRequest = function(){
var ret = this.fn.apply(this,arguments);
if(ret === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}
return ret;
}
//现在我们把3个函数分别包装成职责链节点:
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
// 然后指定节点在职责链中的顺序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
//最后把请求传递给第一个节点:
chainOrder500.passRequest(1,true,500); // 亲爱的用户,您中奖了100元红包了
chainOrder500.passRequest(2,true,500); // 亲爱的用户,您中奖了20元红包了
chainOrder500.passRequest(3,true,500); // 亲爱的用户,您已抽到10元优惠卷
chainOrder500.passRequest(1,false,0); // 亲爱的用户,请再接再厉哦
分别编写order500,order200,orderNormal三个函数,在函数内分别处理自己的业务逻辑,如果自己的函数不能处理的话,就返回字符串nextSuccessor 往后面传递,然后封装Chain这个构造函数,传递一个fn这个对象实列进来,且有自己的一个属性successor,原型上有2个方法 setNextSuccessor 和 passRequest;setNextSuccessor 这个方法是指定节点在职责链中的顺序的,把相对应的方法保存到this.successor这个属性上,chainOrder500.setNextSuccessor(chainOrder200);chainOrder200.setNextSuccessor(chainOrderNormal);指定链中的顺序,因此this.successor引用了order200这个方法和orderNormal这个方法,因此第一次chainOrder500.passRequest(1,true,500)调用的话,调用order500这个方法,直接输出,第二次调用chainOrder500.passRequest(2,true,500);这个方法从链中首节点order500开始不符合,就返回successor字符串,然后this.successor && this.successor.passRequest.apply(this.successor,arguments);就执行这句代码;上面我们说过this.successor这个属性引用了2个方法 分别为order200和orderNormal,因此调用order200该方法,所以就返回了值,依次类推都是这个原理。那如果以后我们想充值300元的红包的话,我们可以编写order300这个函数,然后实列一下链chain包装起来,指定一下职责链中的顺序即可,里面的业务逻辑不需要做任何处理;
六、命令模式(添加命令 按顺序执行)
命令模式中的命令指的是一个执行某些特定事情的指令。
命令模式使用的场景有:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道请求的操作是什么,此时希望用一种松耦合的方式来设计程序代码;使得请求发送者和请求接受者消除彼此代码中的耦合关系。
理解命令模式:
我们经常会在天猫上购买东西,然后下订单,下单后我就想收到货,并且希望货物是真的,对于用户来讲它并关心下单后卖家怎么发货,当然卖家发货也有时间的,比如24小时内发货等,用户更不关心快递是给谁派送,当然有的人会关心是什么快递送货的; 对于用户来说,只要在规定的时间内发货,且一般能在相当的时间内收到货就可以,当然命令模式也有撤销命令和重做命令,比如我们下单后,我突然不想买了,我在发货之前可以取消订单,也可以重新下单(也就是重做命令);比如我的衣服尺码拍错了,我取消该订单,重新拍一个大码的。
理解宏命令:宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。其实类似把页面的所有函数方法放在一个数组里面去,然后遍历这个数组,依次执行该方法的。
var command1 = {
execute: function(){
console.log(1);
}
};
var command2 = {
execute: function(){
console.log(2);
}
};
var command3 = {
execute: function(){
console.log(3);
}
};
// 定义宏命令,command.add方法把子命令添加进宏命令对象,
// 当调用宏命令对象的execute方法时,会迭代这一组命令对象,
// 并且依次执行他们的execute方法。
var command = function(){
return {
commandsList: [],
add: function(command){
this.commandsList.push(command);
},
execute: function(){
for(var i = 0,commands = this.commandsList.length; i < commands; i+=1) {
this.commandsList[i].execute();
}
}
}
};
// 初始化宏命令
var c = command();
c.add(command1);
c.add(command2);
c.add(command3);
c.execute(); // 1,2,3
七、模板方法模式(子类可以继承这个父类,并且可以在子类中重写父类的方法)
模板方法模式由二部分组成,第一部分是抽象父类,第二部分是具体实现的子类,一般的情况下是抽象父类封装了子类的算法框架,包括实现一些公共方法及封装子类中所有方法的执行顺序,子类可以继承这个父类,并且可以在子类中重写父类的方法,从而实现自己的业务逻辑。
经典的饮料与咖啡
先定义父类,子类继承通用的方法,特殊的方法在子类重写。
var Beverage = function (param) {
var boilWater = function () {
console.log('把水煮沸');
};
var brew = param.brew || function () {
throw new Error('必须传递 brew 方法');
};
var pourInCup = param.pourInCup || function () {
throw new Error('必须传递 pourInCup 方法');
};
var addCondiments = param.addCondiments || function () {
throw new Error('必须传递 addCondiments 方法');
};
var F = function () {
};
F.prototype.init = function () {
boilWater();
brew();
pourInCup();
addCondiments();
};
return F;
};
var Coffee = Beverage({
brew: function () {
console.log('用沸水冲泡咖啡');
},
pourInCup: function () {
console.log('把咖啡倒进杯子');
},
addCondiments: function () {
console.log('加糖和牛奶');
}
});
var coffee = new Coffee();
coffee.init();
使用es6实现
//父类咖啡
class Beverage {
constructor(name) {
//单独调用会报错,所以写constructor里面绑定this,this指向父类
this.init = () => {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) { // 如果挂钩返回 true,则需要调料
this.addCondiments();
}
};
console.log('构造函数的'+name)
};
//共用boilWater,子类不会修改它
boilWater() {
console.log('把水煮沸');
};
brew() {
throw new Error('子类必须重写 brew 方法');
};
pourInCup() {
throw new Error('子类必须重写 pourInCup 方法');
};
addCondiments() {
throw new Error('子类必须重写 addCondiments 方法');
};
customerWantsCondiments() {
return true; // 默认需要调料
};
//init也可以写在这里,解构之后单独调用时,在es5 ,this会指向window,在es6,this不会指向window,会报错
// init() {
// console.log(this);
// this.boilWater();
// this.brew();
// this.pourInCup();
// if (this.customerWantsCondiments()) { // 如果挂钩返回 true,则需要调料
// this.addCondiments();
// }
// };
}
//子类咖啡,继承父类饮料
class CoffeeWithHook extends Beverage {
constructor(name) {
//调用父类的构造函数,演示一下super怎么用,下面传了‘名字’字符串
super(name);
}
brew() {
console.log('用沸水冲泡咖啡');
};
pourInCup() {
console.log('把咖啡倒进杯子');
};
addCondiments() {
console.log('加糖和牛奶');
};
customerWantsCondiments() {
return window.confirm('请问需要调料吗?');
};
};
let coffeeWithHook = new CoffeeWithHook('名字');
//如果不把init写在constructor里面的,下面的解构会报错。在es5 ,this会指向window,在es6,this不会指向window,会报错
let {init} = coffeeWithHook;//var init = coffeeWithHook.init
init();
八、策略模式(规则目标一致就可以使用策略模式来封装)
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
- 优点:
- 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句。
- 策略模式提供了开放-封闭原则,使代码更容易理解和扩展。
- 策略模式中的代码可以复用。
var obj = {
"A": function(salary) {
return salary * 4;
},
"B" : function(salary) {
return salary * 3;
},
"C" : function(salary) {
return salary * 2;
}
};
var calculateBouns =function(level,salary) {
return obj[level](salary);
};
console.log(calculateBouns('A',10000)); // 40000
策略模式指的是定义一系列的算法,并且把它们封装起来,但是策略模式不仅仅只封装算法,我们还可以对用来封装一系列的业务规则,只要这些业务规则目标一致,我们就可以使用策略模式来封装它们;
九、发布订阅者模式
发布—订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
- 优点:
- 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
- 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变;同理卖家(发布者)它只需要将鞋子来货的这件事告诉订阅者(买家),他不管买家到底买还是不买,还是买其他卖家的。只要鞋子到货了就通知订阅者即可。
- 缺点:
- 首先要想好谁是发布者
- 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者
- 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
十、中介者模式
中介者模式主要用于一个系统中存在大量的对象,而且这些大量的对象需要互相通信,因为两个对象需 要通信,一个对象必须要持有另一个对象,这样就会导致,系统里,每个对象都互相引用,会引起混乱, 中介者把所有的对象都统一管理起来,其他的对象通过中介者去和别的对象通信。
中介者模式对于我们日常生活中经常会碰到,比如我们去房屋中介去租房,房屋中介人在租房者和房东
出租者之间形成一条中介;租房者并不关心租谁的房,房东出租者也并不关心它租给谁,因为有中介,所
以需要中介来完成这场交易。
中介者模式的作用是解除对象与对象之间的耦合关系,增加一个中介对象后,所有的相关对象都通过中介者对象来通信,而不是相互引用,所以当一个对象发送改变时,只需要通知中介者对象即可。中介者使各个对象之间耦合松散,而且可以独立地改变它们之间的交互。
function A(mediator) {
this.mediator = mediator;
}
A.prototype = {
send: function(msg,receiver) {
this.mediator.send(msg,'A',receiver);
},
receiveMsg: function(msg,sender) {
console.log('我是a',sender+" say:"+msg)
}
}
function B(mediator) {
this.mediator = mediator;
}
B.prototype = {
send: function(msg,receiver) {
this.mediator.send(msg,'B',receiver);
},
receiveMsg: function(msg,sender) {
console.log('我是b',sender+" say:"+msg)
}
}
function Mediator() {
this.A = new A(this);
this.B = new B(this);
}
Mediator.prototype = {
send: function(msg,sender,receiver) {
try {
this[receiver].receiveMsg(msg,sender);
}
catch(err) {
console.log('receiver '+receiver+' is not exsit');
this[sender].receiveMsg('receiver '+ receiver +' is not exsit','mediator');
}
}
}
var _mediator = new Mediator();
var _a = new A(_mediator);
var _b = new B(_mediator);
_a.send('hello i am A','B'); //我是A B say:hello i am B
_b.send('hello i am B','A'); //我是B A say:hello i am A
其中A,B可以抽象出一个公共的父类,这要看A,B有没有共同的地方,Mediator还可以根据对象间的通信的,类似于路由器或交换机一样,中介者需要知道通信者的发起者和接收者,就像路由器需要源ip和目的ip一样,不需要事先把方法写死,因为对象太多的时候,这种方法会导致中介者过于庞大,中介者应该抽象出对象间通信的各种功能,然后把接口对对象开放。
最后推荐一个 js常用的utils合集,帮我点个star吧~