Javascript设计模式
一.理解工厂模式
工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。 简单的工厂模式可以理解为解决多个相似的问题;这也是她的优点;
比如如下代码:
unction CreatePerson(name,age,sex) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sex = sex;
obj.sayName = function(){
return this.name;
}
return obj;
}
var p1 = new CreatePerson("longen",‘28‘,‘男‘);
var p2 = new CreatePerson("tugenhua",‘27‘,‘女‘);
console.log(p1.name); // longen
console.log(p1.age); // 28
console.log(p1.sex); // 男
console.log(p1.sayName()); // longen
console.log(p2.name); // tugenhua
console.log(p2.age); // 27
console.log(p2.sex); // 女
console.log(p2.sayName()); // tugenhua
// 返回都是object 无法识别对象的类型 不知道他们是哪个对象的实列
console.log(typeof p1); // object
console.log(typeof p2); // object
console.log(p1 instanceof Object); // true
如上代码:函数CreatePerson能接受三个参数name,age,sex等参数,可以无数次调用这个函数,每次返回都会包含三个属性和一个方法的对象。
工厂模式是为了解决多个类似对象声明的问题;也就是为了解决实列化对象产生重复的问题。
优点:能解决多个相似的问题。
缺点:不能知道对象识别的问题(对象的类型不知道)。
复杂的工厂模式定义是:将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。
父类只对创建过程中的一般性问题进行处理,这些处理会被子类继承,子类之间是相互独立的,具体的业务逻辑会放在子类中进行编写。
父类就变成了一个抽象类,但是父类可以执行子类中相同类似的方法,具体的业务逻辑需要放在子类中去实现;比如我现在开几个自行车店,那么每个店都有几种型号的自行车出售。我们现在来使用工厂模式来编写这些代码;
父类的构造函数如下:
// 定义自行车的构造函数
var BicycleShop = function(){};
BicycleShop.prototype = {
constructor: BicycleShop,
/*
* 买自行车这个方法
* @param {model} 自行车型号
*/
sellBicycle: function(model){
var bicycle = this.createBicycle(mode);
// 执行A业务逻辑
bicycle.A();
// 执行B业务逻辑
bicycle.B();
return bicycle;
},
createBicycle: function(model){
throw new Error("父类是抽象类不能直接调用,需要子类重写该方法");
}
};
上面是定义一个自行车抽象类来编写工厂模式的实列,定义了createBicycle这个方法,但是如果直接实例化父类,调用父类中的这个createBicycle方法,会抛出一个error,因为父类是一个抽象类,他不能被实列化,只能通过子类来实现这个方法,实现自己的业务逻辑,下面我们来定义子类,我们学会如何使用工厂模式重新编写这个方法,首先我们需要继承父类中的成员,然后编写子类;如下代码:
// 定义自行车的构造函数
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("龙恩");
console.log(childClass);
工厂模式最重要的优点是:可以实现一些相同的方法,这些相同的方法我们可以放在父类中编写代码,那么需要实现具体的业务逻辑,那么可以放在子类中重写该父类的方法,去实现自己的业务逻辑;使用专业术语来讲的话有2点:第一:弱化对象间的耦合,防止代码的重复。在一个方法中进行类的实例化,可以消除重复性的代码。第二:重复性的代码可以放在父类去编写,子类继承于父类的所有成员属性和方法,子类只专注于实现自己的业务逻辑。
单例模式
单例模式的定义是保证一个类只有一个实例,并且提供一个访问它的全局访问点。有些时候一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。单例模式的优点是:
可以用来划分命名空间,减少全局变量的数量
使用单例模式可以使代码组织的更为一致,使代码容易阅读和维护
可以被实例化,且实例化一次
要实现一个标准的单例模式并不复杂,无非是用一个变量标识当前是否已经为某个类创建过对象,如果是,则在下一次获取这个类的实例时,直接返回之前创建的对象。下面是单例模式的基本结构:
// 单例模式
var Singleton = function(name){
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function(){
return this.name;
};
// 获取实例对象
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
};
// 测试单例模式的实例
var a = Singleton.getInstance("aa");
var b = Singleton.getInstance("bb");
实际上因为单例模式是只实例化一次,所以a和b确实是相等的,也就是说下面语句的值为true。
console.log(a===b)
由于单例模式只实例化一次,因此第一次调用,返回的是a实例的对象,继续调用的时候,b的实例也就是a的实例,因此下面打印的都是aa:
console.log(a.getName());// aa
console.log(b.getName());// aa
观察者模式
观察者模式又叫做发布-订阅模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于他的对象都将得到通知,在javascript的开发中,一般用事件模型来替代传统的发布 — 订阅模式。
发布 — 订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅 ajax请求的 error 、 succ 等事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布 — 订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
发布 — 订阅模式还可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布 — 订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
实际上,只要我们曾经在 DOM 节点上面绑定过事件函数,那我们就曾经使用过发布 — 订阅模式,以下代码便是一个示例:
document.body.addEventListener( ‘click‘, function(){
alert(2);}, false );
document.body.click(); // 模拟用户点击
在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅 document.body 上的 click 事件,当 body 节点被点击时, body 节点便会向订阅者发布这个消息。就像是楼房购买,购房者不知道房子什么时候开售,于是他在订阅消息后等待售楼处发布消息。
除了 DOM 事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的发布 —订阅模式可以用于任何 JavaScript代码中。实现发布 — 订阅模式的步骤如下:
1. 首先指定好谁充当发布者;
2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者;
3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function( fn ){ // 增加订阅者
this.clientList.push( fn ); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function(){ // 发布消息
for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
fn.apply( this, arguments ); // arguments 是发布消息时带上的参数
}
};
//调用
salesOffices.listen( function( price, squareMeter ){//订阅消息
console.log( ‘价格= ‘ + price );
console.log( ‘squareMeter= ‘ + squareMeter );
});
salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米
至此,实现了最简单的发布-订阅模式。比起在Java中实现观察者模式,还是有不同的,在Java里面实现,通常会把订阅者对象当成引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如 update的方法,供发布者对象在适合的时候调用。而在 JavaScript中,我们用注册回调函数的形式来代替传统的发布 — 订阅模式。
命令模式
命令模式中的命令指的是一个执行某些特定事情的指令。
命令模式使用的场景有:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道请求的操作是什么,此时希望用一种松耦合的方式来设计程序代码;使得请求发送者和请求接受者消除彼此代码中的耦合关系。
我们先来列举生活中的一个列子来说明下命令模式:比如我们经常会在天猫上购买东西,然后下订单,下单后我就想收到货,并且希望货物是真的,对于用户来讲它并关心下单后卖家怎么发货,当然卖家发货也有时间的,比如24小时内发货等,用户更不关心快递是给谁派送,当然有的人会关心是什么快递送货的; 对于用户来说,只要在规定的时间内发货,且一般能在相当的时间内收到货就可以,当然命令模式也有撤销命令和重做命令,比如我们下单后,我突然不想买了,我在发货之前可以取消订单,也可以重新下单(也就是重做命令);比如我的衣服尺码拍错了,我取消该订单,重新拍一个大码的。
1. 命令模式的列子
记得我以前刚做前端的那会儿,也就是刚毕业进的第一家公司,进的是做外包项目的公司,该公司一般外包淘宝活动页面及腾讯的游戏页面,我们那会儿应该叫切页面的前端,负责做一些html和css的工作,所以那会儿做腾讯的游戏页面,经常会帮他们做静态页面,比如在页面放几个按钮,我们只是按照设计稿帮腾讯游戏哪方面的把样式弄好,比如说页面上的按钮等事情,比如说具体说明的按钮要怎么操作,点击按钮后会发生什么事情,我们并不知道,我们不知道他们的业务是什么,当然我们知道的肯定会有点击事件,具体要处理什么业务我们并不知道,这里我们就可以使用命令模式来处理了:点击按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但是目前我们并不知道接收者是什么对象,也不知道接受者究竟会做什么事情,这时候我们可以使用命令模式来消除发送者与接收者的代码耦合关系。
我们先使用传统的面向对象模式来设计代码:
假设html结构如下:
<button id="button1">刷新菜单目录</button>
<button id="button2">增加子菜单</button>
<button id="button3">删除子菜单</button>
JS代码如下:
var b1 = document.getElementById("button1"),
b2 = document.getElementById("button2"),
b3 = document.getElementById("button3");
// 定义setCommand 函数,该函数负责往按钮上面安装命令。点击按钮后会执行command对象的execute()方法。
var setCommand = function(button,command){
button.onclick = function(){
command.execute();
}
};
// 下面我们自己来定义各个对象来完成自己的业务操作
var MenuBar = {
refersh: function(){
alert("刷新菜单目录");
}
};
var SubMenu = {
add: function(){
alert("增加子菜单");
},
del: function(){
alert("删除子菜单");
}
};
// 下面是编写命令类
var RefreshMenuBarCommand = function(receiver){
this.receiver = receiver;
};
RefreshMenuBarCommand.prototype.execute = function(){
this.receiver.refersh();
}
// 增加命令操作
var AddSubMenuCommand = function(receiver) {
this.receiver = receiver;
};
AddSubMenuCommand.prototype.execute = function() {
this.receiver.add();
}
// 删除命令操作
var DelSubMenuCommand = function(receiver) {
this.receiver = receiver;
};
DelSubMenuCommand.prototype.execute = function(){
this.receiver.del();
}
// 最后把命令接收者传入到command对象中,并且把command对象安装到button上面
var refershBtn = new RefreshMenuBarCommand(MenuBar);
var addBtn = new AddSubMenuCommand(SubMenu);
var delBtn = new DelSubMenuCommand(SubMenu);
setCommand(b1,refershBtn);
setCommand(b2,addBtn);
setCommand(b3,delBtn);
从上面的命令类代码我们可以看到,任何一个操作都有一个execute这个方法来执行操作;上面的代码是使用传统的面向对象编程来实现命令模式的,命令模式过程式的请求调用封装在command对象的execute方法里。我们有没有发现上面的编写代码有点繁琐呢,我们可以使用javascript中的回调函数来做这些事情的,在面向对象中,命令模式的接收者被当成command对象的属性保存起来,同时约定执行命令的操作调用command.execute方法,但是如果我们使用回调函数的话,那么接收者被封闭在回调函数产生的环境中,执行操作将会更加简单,仅仅执行回调函数即可,下面我们来看看代码如下:
代码如下:
var setCommand = function(button,func) {
button.onclick = function(){
func();
}
};
var MenuBar = {
refersh: function(){
alert("刷新菜单界面");
}
};
var SubMenu = {
add: function(){
alert("增加菜单");
}
};
// 刷新菜单
var RefreshMenuBarCommand = function(receiver) {
return function(){
receiver.refersh();
};
};
// 增加菜单
var AddSubMenuCommand = function(receiver) {
return function(){
receiver.add();
};
};
var refershMenuBarCommand = RefreshMenuBarCommand(MenuBar);
// 增加菜单
var addSubMenuCommand = AddSubMenuCommand(SubMenu);
setCommand(b1,refershMenuBarCommand);
setCommand(b2,addSubMenuCommand);
我们还可以如下使用javascript回调函数如下编码:
// 如下代码上的四个按钮 点击事件
var b1 = document.getElementById("button1"),
b2 = document.getElementById("button2"),
b3 = document.getElementById("button3"),
b4 = document.getElementById("button4");
/*
bindEnv函数负责往按钮上面安装点击命令。点击按钮后,会调用
函数
*/
var bindEnv = function(button,func) {
button.onclick = function(){
func();
}
};
// 现在我们来编写具体处理业务逻辑代码
var Todo1 = {
test1: function(){
alert("我是来做第一个测试的");
}
};
// 实现业务中的增删改操作
var Menu = {
add: function(){
alert("我是来处理一些增加操作的");
},
del: function(){
alert("我是来处理一些删除操作的");
},
update: function(){
alert("我是来处理一些更新操作的");
}
};
// 调用函数
bindEnv(b1,Todo1.test1);
// 增加按钮
bindEnv(b2,Menu.add);
// 删除按钮
bindEnv(b3,Menu.del);
// 更改按钮
bindEnv(b4,Menu.update);
2.理解宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
其实类似把页面的所有函数方法放在一个数组里面去,然后遍历这个数组,依次
执行该方法的。
代码如下:
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
原文:https://www.cnblogs.com/hundred-beats/p/12105220.html