JavaScript常用设计模式
设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。
创建型(Creational Patterns)
单例模式(Singleton Pattern)
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
// 惰性单例
const getSingle = (fn) => {
let result;
return function () {
return result || (result = fn.apply(this, arguments));
};
};
// 冻结对象
Object.freeze(instance);
原型模式(Prototype Pattern)
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
ECMAScript 5提供了Object.create方法,可以用来克隆对象。
// Object.create
var proto = {};
var properties = {
name: {
value: 'name',
writable: true,
enumerable: false,
configurable: false,
},
};
var obj = Object.create(proto, properties);
// 方法继承
Child.prototype = new Parent()
工厂模式(Factory Pattern)
在创建对象时不暴露创建逻辑,而是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替new操作。
// 简单工厂
const UserFactory = function (role) {
function User(opt) {
this.name = opt.name;
}
switch (role) {
case 'admin':
return new User({ name: '管理员');
break;
default:
throw new Error('参数错误')
}
}
// 工厂方法(安全模式)
const UserFactory = function(role) {
if(this instanceof UserFactory) {
return new this[role]();
} else {
return new UserFactory(role);
}
}
UserFactory.prototype = {}
let admin = UserFactory('Admin')
结构型(Structural Patterns)
代理模式(Proxy Pattern)
为一个对象提供一个代用品或占位符,以便控制对它的访问。
保护代理:用于控制不同权限的对象对目标对象的访问;
虚拟代理:可应用于图片懒加载、惰性加载、合并http请求等;
缓存代理:可应用于缓存ajax异步请求数据、计算乘积等;
// 缓存代理工厂
const createProxy = (fn) => {
const cache = [];
return function () {
const args = [].join.call(arguments, ',');
if (args in cache) {
return cache[args];
}
return (cache[args] = fn.apply(this, arguments));
};
};
适配器模式(Adapter Pattern)
解决两个软件实体间的接口、方法不兼容的问题。
// 接口适配
const user1 = { name: 'user1' };
const user2 = { username: 'user2' };
function getName(param) {
return param.name
}
function adapter(param) {
return { name: param.username };
}
getName(user1);
getName(adapter(user2));
相似模式区分
装饰者模式:为了给对象增加功能,常常形成一条长的装饰链;
适配器模式:用来解决两个已有接口之间不匹配的问题,通常只包装一次;
外观模式:定义了一个新的接口;
装饰器模式(Decorator Pattern)
在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
const user = {
name: 'username',
getName() {
return this.name;
},
};
function addDecorator(user) {
const prevName = user.getName();
user.getName = function () {
return prevName + ' with decorator';
};
}
用AOP装饰函数
Function.prototype.before = function (beforefn) {
var self = this;
return function () {
beforefn.apply(this, arguments);
return self.apply(this, arguments);
};
};
Function.prototype.after = function (afterfn) {
var self = this;
return function () {
var ret = self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};
var before = function (fn, beforefn) {
return function () {
beforefn.apply(this, arguments);
return fn.apply(this, arguments);
};
};
外观模式(Facade Pattern)
外观模式为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口使得这一子系统更加容易使用,他可以将一些复杂的操作封装起来,并创建一个简单的接口用于调用。
外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过请求外观接口来达到访问子系统的目的。但在一段使用了外观模式的程序中,请求外观并不是强制的。如果外观不能满足客户的个性化需求,那么客户也可以选择越过外观来直接访问子系统。
- 为一组子系统提供一个简单便利的访问入口
- 隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节
function addEvent(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;
}
}
组合模式(Composite Pattern)
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。
1)组合模式不是父子关系,它们能够合作的关键是拥有相同的接口;
2)对叶对象操作的一致性,要对每个目标对象都实行同样的操作;
3)可用中介者模式处理双向映射关系,例如一个子节点同时在不同的父节点中存在(组织架构);
4)可用职责链模式提高组合模式性能,通过设置链条避免每次都遍历整个树;
何时使用组合模式
- 表示对象的部分-整体层次结构。
- 客户希望统一对待树中的所有对象。
享元模式(Flyweight Pattern)
享元模式是一种用于性能优化的模式,核心是运用共享技术来有效支持大量细粒度的对象。
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性),目标是尽量减少共享对象的数量。
1)内部状态储存于对象内部。
2)内部状态可以被一些对象共享。
3)内部状态独立于具体的场景,通常不会改变。
4)外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
使用场景
- 一个程序中使用了大量的相似对象
- 由于使用了大量的对象,造成了很大的内存开销
- 对象的大多数状态都可以变为外部状态
- 剥离出对象的外部状态之后,可以使用相对较少的共享对象取代大量对象
对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接 new ,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入池子等待被下次获取。
// 对象池缓存对象
class colorFactory {
constructor(name) {
this.colors = {};
}
create(name) {
let color = this.colors[name];
if (color) return color;
this.colors[name] = new Color(name);
return this.colors[name];
}
};
行为型(Behavioral Patterns)
策略模式(Strategy Pattern)
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
const strategies = {
strategy: () => {},
};
const useStrategy = (fn, args) => fn(args);
useStrategy(strategies.strategy);
模板方法模式(Template Pattern)
模板方法是基于继承的设计模式,通过封装变化提高系统扩展性
1)抽象父类,封装子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序;
2)具体的实现子类,子类通过继承抽象父类,继承整个算法结构,并且可以选择重写父类的方法;
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 wantsCondiments = param.wantsCondiments || function () {
return true;
};
var F = function () {};
F.prototype.init = function () {
boilWater();
brew();
pourInCup();
// 钩子方法 hook
if (wantsCondiments()) {
addCondiments();
}
};
return F;
};
var Tea = Beverage({
brew: function () {
console.log('用沸水浸泡茶叶');
},
pourInCup: function () {
console.log('把茶倒进杯子');
},
addCondiments: function () {
console.log('加柠檬');
},
wantsCondiments: function () {
return window.confirm('请问需要调料吗?');
},
});
var tea = new Tea();
tea.init();
使用钩子方法(hook)是隔离变化的一种常见手段。在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。
迭代器模式(Iterator Pattern)
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
内部迭代器:内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用;
外部迭代器:必须显式地请求迭代下一个元素;
// 内部迭代器
const each = (arr, callback) => {
for (let i = 0, len = arr.length; i < len; i++) {
// callback 的执行结果返回false,提前终止迭代
if (callback.call(arr[i], i, arr[i]) === false) {
break;
}
}
};
// 外部迭代器
const Iterator = (obj) => {
let curr = 0;
const next = () => {
curr += 1;
};
const isDone = () => curr === obj.length;
const getCurrItem = () => obj[curr];
return { next, isDone, getCurrItem, length: obj.length };
};
发布-订阅模式(Publish-Subscribe Pattern)
它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
var pubsub = (function () {
var clientList = {};
return {
subscribe(key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
},
publish() {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; (fn = fns[i++]); ) {
fn.apply(this, arguments);
}
},
unsubscribe(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--) {
fns[i] === fn && fns.splice(i, 1);
}
}
},
};
})();
发布-订阅模式和观察者模式的不同
发布-订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。
命令模式(Command Pattern)
将方法的调用、请求或者操作封装到一个单独的对象中,给我们酌情执行同时参数化和传递方法调用的能力。
命令(command)指的是一个执行某些特定事情的指令。
宏命令是一组命令的集合,通过执行宏命令的方式,可以依次执行一批命令
const RefreshMenuBarCommand = (receiver) => ({
execute() {
receiver.refresh();
},
});
const setCommand = (button, command) => {
button.onclick = function () {
command.execute();
};
};
setCommand(button, RefreshMenuBarCommand(MenuBar));
状态模式(State Pattern)
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
1)将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化;
2)从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果。
var Light = function () {
this.currState = FSM.off;
this.button = null;
};
Light.prototype.init = function () {
var self = this,
button = document.createElement('button');
button.innerHTML = '已关灯';
this.button = document.body.appendChild(button);
this.button.onclick = function () {
self.currState.buttonWasPressed.call(self); // 把请求委托给FSM状态机
};
};
var FSM = {
off: {
buttonWasPressed: function () {
console.log('关灯');
this.currState = FSM.on;
this.button.innerHTML = '下一次按我是开灯';
},
},
on: {
buttonWasPressed: function () {
console.log('开灯');
this.currState = FSM.off;
this.button.innerHTML = '下一次按我是关灯';
},
},
};
var light = new Light();
light.init();
状态模式可用来优化条件分支语句、含有大量状态且行为随状态改变而改变的场景,关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
状态模式和策略模式
共同点:都封装了一系列的算法或者行为,都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行;
不同点:策略模式中的各个策略类是平等又平行的,他们之间没有任何联系,算法切换是用户主动完成的;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部,客户不需要了解这些细节;
中介者模式(Mediator Pattern)
中介者模式的作用就是解除对象与对象之间的紧耦合关系。以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。
var goods = {
'red|32G': 3,
'red|16G': 0,
'blue|32G': 1,
'blue|16G': 6,
};
var colorSelect = document.getElementById('colorSelect'),
memorySelect = document.getElementById('memorySelect'),
numberInput = document.getElementById('numberInput');
var mediator = (function () {
var colorInfo = document.getElementById('colorInfo'),
memoryInfo = document.getElementById('memoryInfo'),
numberInfo = document.getElementById('numberInfo'),
nextBtn = document.getElementById('nextBtn');
return {
changed: function (obj) {
var color = colorSelect.value,
memory = memorySelect.value,
number = numberInput.value,
stock = goods[color + '|' + memory];
if (obj === colorSelect) {
colorInfo.innerHTML = color;
} else if (obj === memorySelect) {
memoryInfo.innerHTML = memory;
} else if (obj === numberInput) {
numberInfo.innerHTML = number;
}
if (!color) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择手机颜色';
return;
}
if (!memory) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请选择内存大小';
return;
}
if (Number.isInteger(number - 0) && number > 0) {
nextBtn.disabled = true;
nextBtn.innerHTML = '请输入正确的购买数量';
return;
}
if (number > stock) {
nextBtn.disabled = true;
nextBtn.innerHTML = '库存不足';
return;
}
nextBtn.disabled = false;
nextBtn.innerHTML = '放入购物车';
},
};
})();
colorSelect.onchange = function () {
mediator.changed(this);
};
memorySelect.onchange = function () {
mediator.changed(this);
};
numberInput.oninput = function () {
mediator.changed(this);
};
职责链模式(Chain of Responsibility Pattern)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
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.next();
}
return ret;
};
Chain.prototype.next = function () {
return (
this.successor &&
this.successor.passRequest.apply(this.successor, arguments)
);
};