写在前面
设计模式是不分语言的,本文介绍的是14种设计模式,几乎涵盖了js中涉及到所有设计模式,部分设计模式代码实践部分也会分别,用面向对象思维 和 js"鸭子类型"思维 两种代码对比同一种设计模式。内容较长,读完定有收获!
js中的this
跟别的语言大相径庭的是,JavaScript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
- this 的指向,具体到实际应用中,this的指向大致可以分为以下4种。
❏ 作为对象的方法调用。
❏ 作为普通函数调用。
❏ 构造器调用。
❏ Function.prototype.call或Function.prototype.apply调用。下面我们分别进行介绍。
- 1.作为对象方法调用,this 指向该对象
var obj = {
a:1,
getA:function(){
console.log(this==obj);//输入:true
console.log(this.a);//输出:1
}
}
obj.getA();
- 2.作为普通函数调用,指向全局对象。在浏览器中,这个全局对象就是window对象
window.name = 'windowNmae';
var getName = function(){
return this.name;
}
console.log(getName());//输出: windowNmae
- 3.构造器调用,构造器里的this就指向返回的这个对象
var MyClass = function(){
this.name = 'MyClass'
};
var obj = new MyClass();
console.log(obj.name);//输出: MyClass
- Function.prototype.call或Function.prototype.apply调用,可以动态地改变传入函数的this
var obj1 = {
name:'sven',
getName:function(){
return this.name;
}
};
var obj2 = {
name:'anne'
};
console.log(obj1.getName());//输出: sven
console.log(obj1.getName.call(obj2));//输出: anne
设计模式介绍
1.单利模式(惰性单利)
var Singleton = function(name){
this.name = name
}
Singleton.getSingle = (function(){
var instance = null;
return function(name){
if(!instance){
instance = new Singleton(name);
}
return instance;
}
})();
var singleton1 = Singleton.getSingle('app1');
var singleton2 = Singleton.getSingle('app2');
console.log(singleton1==singleton2);//输出:true
console.log(singleton1.name,singleton2.name);//输出:app1 app1
2.策略模式
很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,而绩效为B的人年终奖是2倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖
- 面向对象语言思想实现
// 策略类
var performanceS = function(){}
performanceS.prototype.calculate = function(salary){
return salary * 4;
}
var performanceA = function(){}
performanceA.prototype.calculate = function(salary){
return salary * 3;
}
var performanceB = function(){}
performanceB.prototype.calculate = function(salary){
return salary * 2;
}
// 奖金类
var Bonus = function(){
this.salary = null;//原始工资
this.strategy = null;//绩效对应的策略对象
}
//设置原始工资
Bonus.prototype.setSalary = function(salary){
this.salary = salary;
}
//设置绩效等级对象的策略对象
Bonus.prototype.setStrategy = function(strategy){
this.strategy = strategy;
}
//取得奖励
Bonus.prototype.getBonus = function(){
//把计算奖励的操作委托给策略对象
return this.strategy.calculate(this.salary)
}
//test
var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS())
console.log(bonus.getBonus());//输出:40000
bonus.setStrategy(new performanceA())
console.log(bonus.getBonus());//输出:30000
- js 版本策略模式
var strategies = {
'S':function(salary){
return salary * 4;
},
'A':function(salary){
return salary * 3;
},
'B':function(salary){
return salary * 2;
}
}
var calculateBonus = function(level,salary){
return strategies[level](salary);
}
console.log(calculateBonus('S',10000));//输出:40000
console.log(calculateBonus('A',10000));//输出:30000
3.代理模式
代理就是委托别人do,这里我们讨论常用的两种代理
- 3.1虚拟代理
在Web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。
下面我们来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口,外界调用这个接口,便可以给该img标签设置src属性:
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.append(imgNode);
return {
setSrc:function(src){
imgNode.src = src;
}
}
})();
myImage.setSrc('https://himg.bdimg.com/sys/portrait/item/ca253731393330373830351216');
我们把网速调至5KB/s,然后通过MyImage.setSrc给该img节点设置src,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的菊花图loading.gif,来提示用户图片正在加载。代码如下:
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.append(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('file://loading.gif');
img.src = src
}
}
})()
proxyImage.setSrc('https://himg.bdimg.com/sys/portrait/item/ca253731393330373830351216')
- 3.2 缓存代理-计算乘积
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
var mult = function(){
var a = 1;
for(var i=0;i<arguments.length;i++){
a *= arguments[i];
}
return a;
}
console.log(mult(2,3));//输出:6
console.log(mult(2,3,4));//输出:24
现在加入缓存代理函数:
var proxyMult = (function(){
var cache = {};
return function(){
var arg = Array.prototype.join.call(arguments,',');
if(arg in cache){
return cache[arg];
}
return cache[arg] = mult.apply(this,arguments)
}
})();
console.log(proxyMult(1,2,3,4));//输出:24
console.log(proxyMult(1,2,3,4));//输出:24
通过增加缓存代理的方式,mult函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。
4.迭代器模式
目前的绝大部分语言都内置了迭代器,这里简单介绍下倒叙迭代
var reverseEach = function(ary,callback){
for(var i=ary.length-1;i>=0;i--){
callback(i,ary[i])
}
}
reverseEach([0,1,2],function(i,n){
console.log(`i = ${i} n = ${n}`);
/* 输出
i = 2 n = 2
i = 1 n = 1
i = 0 n = 0
*/
})
5.发布-订阅模式
实际开发经常用到的,可以直接拿来用到项目中
// 全局的发布订阅对象
var EventBus = (function(){
var clientList = {},
listen,
trigger,
remove;
listen = function(key,fn){
if(!clientList[key]){
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function(){
var key = Array.prototype.shift.apply(arguments);
var fns = clientList[key];
if(!fns || fns.length==0){
return false;
}
for(var i=0;i<fns.length;i++){
fns[i].apply(this,arguments)
}
};
remove = function(key,fn){
var fns = clientList[key];
if(!fns){
return false;
}
if(!fn){
fns&&(fns.length=0);
}else {
for(var i=fns.length-1;i<=0;i--){
if(fn==fns[i]){
fns.splice(i,1)
}
}
}
};
return {
listen:listen,
trigger:trigger,
remove:remove
}
})()
var callback = function(price){
console.log(`价格 = ${price}`)
}
EventBus.listen('sq88',callback);
EventBus.trigger('sq88',20000);
// EventBus.remove('sq88',callback);
EventBus.trigger('sq88',20000);
6. 命令模式
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
var closeDoorCommmand = {
execute: function(){
console.log('关门');
}
}
var openPcCommand = {
execute: function(){
console.log('开电脑');
}
}
var openQQCommand = {
execute: function(){
console.log('登录QQ');
}
}
var MacroCommand = function(){
return {
commandsList:[],
add:function(command){
this.commandsList.add(command);
},
execute:function(){
for(var i=0;i<this.commandsList.length;i++){
var command = this.commandsList[i];
command.execute();
}
}
}
}
var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommmand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute();
7.组合模式
在程序设计中,也有一些和“事物是由相似的子事物构成”类似的思想。组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的。
8.模板方法模式
在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好地体现了泛化的思想。
var Beverage = function(){}
Beverage.prototype.boilWater = function(){
console.log('把是煮沸');
}
Beverage.prototype.brew = function(){
throw new Error('子类必须重写brew方法');
}
Beverage.prototype.pourInCup = function(){
throw new Error('子类必须重写pourInCup方法');
}
Beverage.prototype.customeerWantsCondiments = function(){
return true;//默认需要调料
}
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
if(this.customeerWantsCondiments()){
this.addComponent();
}
}
var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
console.log('用沸水冲泡咖啡');
}
CoffeeWithHook.prototype.pourInCup = function(){
console.log('把咖啡倒进杯子');
}
CoffeeWithHook.prototype.customeerWantsCondiments = function(){
return window.confirm('请问需要调料吗?');
}
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
9.享元模式
元模式的核心是运用共享技术来有效支持大量细粒度的对象。
享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。❏ 内部状态存储于对象内部。❏ 内部状态可以被一些对象共享。❏ 内部状态独立于具体的场景,通常不会改变。❏ 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。
假设有个内衣工厂,目前的产品有50种男式内衣和50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。正常情况下需要50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。不使用享元模式的情况下,在程序里也许会这样写:
var Model = function(sex,underwear){
this.sex = sex;
this.underwear = underwear;
}
Model.prototype.takePhoto = function(){
console.log(`sex = ${this.sex} underwear = ${this.underwear}`);
}
for(var i=0;i<=50;i++){
var maleModel = new Model('male','underwear' + i);
maleModel.takePhoto();
}
for(var i=0;i<=50;i++){
var femaleModel = new Model('female','underwear' + i);
femaleModel.takePhoto();
}
要得到一张照片,每次都需要传入sex和underwear参数,如上所述,现在一共有50种男内衣和50种女内衣,所以一共会产生100个对象。如果将来生产了10000种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。下面我们来考虑一下如何优化这个场景。虽然有100种内衣,但很显然并不需要50个男模特和50个女模特。其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。
var Model = function(sex){
this.sex = sex;
}
Model.prototype.takePhoto = function(){
console.log(`sex = ${this.sex} underwear = ${this.underwear}`);
}
var maleModel = new Model('male');
var femaleModel = new Model('female');
for(var i=0;i<50;i++){
maleModel.underwear = 'underwear' + i;
maleModel.takePhoto();
}
for(var i=0;i<50;i++){
femaleModel.underwear = 'underwear' + i;
femaleModel.takePhoto();
}
10.职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
- 异步的职责链
而在现实开发中,我们经常会遇到一些异步的问题,比如我们要在节点函数中发起一个ajax异步请求,异步请求返回的结果才能决定是否继续在职责链中passRequest。
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;
}
Chain.prototype.next = function(){
return this.successor && this.successor.passRequest.apply(this.successor,arguments);
}
var fn1 = new Chain(function(){
console.log(1);
return 'nextSuccessor'
})
var fn2 = new Chain(function(){
console.log(2);
var self = this;
setTimeout(function(){
self.next();
},1000);
})
var fn3 = new Chain(function(){
console.log(3);
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest();
11.中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系
12.装饰器模式
这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式
Function.prototype.before = function(beforeFn){
var that = this;//保存原函数的引用
return function(){//返回包含了原函数和新函数的“代理”函数
beforeFn.apply(this,arguments);//执行新函数,且保证this不被劫持,函数接收的参数
return that.apply(this,arguments);//执行原函数并返回原函数的执行结果,且保证this不被劫持
}
}
Function.prototype.after = function(afterFn){
var that = this;
return function(){
var ret = that.apply(this,arguments);
afterFn.apply(this,arguments);
return ret;
}
}
function myClick(){
console.log('myclick');
}
myClick();
myClick.before(function(){
console.log('前')
}).after(()=>console.log('后'))();
13. 状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
点灯程序 (弱光 --> 强光 --> 关灯)循环
// 关灯
var OffLightState = function(light) {
this.light = light;
};
// 弱光
var WeakLightState = function(light) {
this.light = light;
};
// 强光
var StrongLightState = function(light) {
this.light = light;
};
var Light = function(){
/* 开关状态 */
this.offLight = new OffLightState(this);
this.weakLight = new WeakLightState(this);
this.strongLight = new StrongLightState(this);
/* 快关按钮 */
this.button = null;
};
Light.prototype.init = function() {
var button = document.createElement("button"),
self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '开关';
this.currentState = this.offLight;
this.button.click = function() {
self.currentState.buttonWasPressed();
}
};
// 让抽象父类的抽象方法直接抛出一个异常(避免状态子类未实现buttonWasPressed方法)
Light.prototype.buttonWasPressed = function() {
throw new Error("父类的buttonWasPressed方法必须被重写");
};
Light.prototype.setState = function(newState) {
this.currentState = newState;
};
/* 关灯 */
OffLightState.prototype = new Light(); // 继承抽象类
OffLightState.prototype.buttonWasPressed = function() {
console.log("关灯!");
this.light.setState(this.light.weakLight);
}
/* 弱光 */
WeakLightState.prototype = new Light();
WeakLightState.prototype.buttonWasPressed = function() {
console.log("弱光!");
this.light.setState(this.light.strongLight);
};
/* 强光 */
StrongLightState.prototype = new Light();
StrongLightState.prototype.buttonWasPressed = function() {
console.log("强光!");
this.light.setState(this.light.offLight);
};
14. 适配器模式
var renderMap = function(map){
if(map.show instanceof Function){
map.show();
}
}
var googleMap = {
show:function(){
console.log('开始渲染谷歌地图');
}
}
var baiduMap = {
display:function(){
console.log('开始渲染百度地图')
}
}
var baiduMapAdapter = {
show:function(){
return baiduMap.display();
}
}
renderMap(googleMap);
renderMap(baiduMapAdapter);