原文,zhuanlan.zhihu.com/p/111553641。
最近看完了《JavaScript 设计模式与开发实践》,也在学习极客时间的专栏《设计模式之美》,想着整理一下常用的设计模式。设计模式不仅受用于面试,更能增进代码水平,是每个程序员必须掌握的。
GoF 在《设计模式》一种归纳了 23 种设计模式,而它们又属于三种类型的模式,分别是创建型模式、结构型模式和行为型模式。正如书中所说,其实每一种设计模式都来自真实项目,作者只是将它们取一个好听的名字,方便记忆与传播。所以设计模式可比数据结构算法上手要简单一点,因为很多设计模式你可能都似曾相似,学习的过程不过是一个接一个的顿悟。
本文中每一种设计模式,我都会搭配一个简单的例子,我会尽量用 JS 实现,如果 JS 不能很好的表达,就用 Java(已替换成TS) 实现。毕竟《设计模式》一书的子标题是“可复用面向对象软件的基础”,它是为面向对象总结的设计模式。
大分类
- 创建型(5)
工厂方法,抽象工厂,单例,建造者,原型。
- 结构型(7)
适配器,装饰器,代理,外观,桥接,组合,享元。
- 行为型(11)
策略,模板,观察者,迭代器,中介者,状态,职责链,命令,访问者,备忘录,解释器。
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。
具体一点,设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

创建型
工厂方法
在 JS 中,工厂方法是创建对象的一种方式。它像工厂一样,生产出来的函数都是标准件(拥有相同的属性)。它和单例模式有一点像,缓存了对象,避免重复重新结构相同的对象。下面是创建不同角色的工厂类。
function createPeopleFactory(id, name, age) {
const obj = new Object();
obj.id = id;
obj.name = name;
obj.age = age;
return obj;
}
const child = createPeopleFactory(1, 'baby', 1);
const father = createPeopleFactory(2, 'peter', 25);
抽象工厂
在工厂方法的基础上再抽象一层,用来管理多个工厂类。平时使用场景很少。
abstract class AbstractFactory {
public abstract getColor(color: string);
public abstract getShape(shape: string);
}
// 通过传递形状或颜色信息来获取工厂
class FactoryProducer {
public static getFactory(choice: string) {
if (choice === 'SHAPE') {
return new ShapeFactory();
} else if (choice === 'COLOR') {
return new ColorFactory();
}
return null;
}
}
class ColorFactory extends AbstractFactory {
public getColor(color) {
// do something
}
public getShape() {
return null;
}
}
class ShapeFactory extends AbstractFactory {
public getColor() {
return null;
}
public getShape(shape) {
// do something
}
}
const shape = FactoryProducer.getFactory('SHAPE');
shape.getShape('CIRCLE');
const color = FactoryProducer.getFactory('COLOR');
color.getColor('RED');
单例
保证一个类仅有一个实例,并提供一个访问它的全局访问点 。
const singleton = function(fn) {
let result = null;
return function() {
return result || (result = fn.apply(this, arguments));
};
};
const getScript = singleton(function() {
return document.createElement('script');
});
const script1 = getScript();
const script2 = getScript();
console.log(script1 === script2); // true
建造者
用来参数需要在构造函数中初始化,但是参数又过多的场景。
未使用建造者模式
class Shape {
constructor(width, height, color, opacity, borderWidth, boxShadow) {
if (typeof width !== 'number') {
throw new Error('width should be a number');
}
this.width = width;
if (typeof height !== 'number') {
throw new Error('width should be a number');
}
this.height = height;
this.color = color;
this.opacity = opacity;
if (width < borderWidth) {
throw new Error('width should be greater than borderWidth');
}
this.borderWidth = borderWidth;
}
}
const shape = new Shape(10, 10, 'red', 1, 10);
使用建造者模式改造后
class ShapeConfig {
constructor(builder) {
this.width = builder.width;
this.height = builder.height;
this.color = builder.color;
this.opacity = builder.opacity;
this.borderWidth = builder.borderWidth;
}
}
class Shape {
constructor() {
this.width = 0;
this.height = 0;
this.color = '';
this.opacity = 1;
this.borderWidth = 0;
}
// 可以把多个值的比较逻辑都放在构建函数中
build() {
if (this.width < this.borderWidth) {
throw new Error('width should be greater than borderWidth');
}
return new ShapeConfig(this);
}
setWidth(width) {
if (typeof width !== 'number') {
throw new Error('width should be a number');
}
this.width = width;
return this;
}
setHeight(height) {
if (typeof height !== 'number') {
throw new Error('height should be a number');
}
this.height = height;
return this;
}
setColor(color) {
this.color = color;
return this;
}
setOpacity(opacity) {
this.opacity = opacity;
return this;
}
setBorderWidth(borderWidth) {
this.borderWidth = borderWidth;
return this;
}
}
const shape = new Shape();
// 可以通过return this方便的实现链式调用
shape
.setWidth(10)
.setHeight(10)
.setColor('red')
.setOpacity(1)
.setBorderWidth(11)
.build();
console.log(shape);
原型
原型模式是用于创建对象的一种模式,通过克隆来创建对象的。JavaScript 就是一种基于原型的语言。但就 JavaScript 的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。我们可以通过Object.create克隆对象。
const Plane = function() {
this.blood = 100;
this.attackLevel = 1;
this.defenseLevel = 1;
};
const plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;
const clonePlane = Object.create(plane);
console.log(clonePlane);
结构型
适配器
适配器英文是 Adapter。顾名思义它就是做适配用的,将一个不可用的接口转成可用的接口。适配器模式是一种 “亡羊补牢”的模式,没有人会在程序的设计之初就使用它。最近前端比较典型的应用是跨端框架,mpvue 和 taro,它们都是在应用和各个小程序以及终端之间建立了一层适配器。
下面举一个支付的例子,我们只需要调用 pay 函数,适配器帮我们平台之间的差异
function pay(id, price) {
const platform = window.platform;
switch (platform) {
case 'wechat':
wx.pay({ id, price });
break;
case 'alipay':
alipay.pay({ id, price });
break;
case 'jd':
jd.pay({ id, price });
break;
case 'xxx':
xxx.toPay({ goodsId: id, price });
break;
}
}
pay(101, 1000);
装饰者
写代码的时候,我们总遵循“组合优于继承”,而装饰者模式就是一种用组合关系的来组织代码。而我们平时所说的装饰器就是装饰者的一种应用。
这个人原先普普通通,经过装饰者模式改造,瞬间变成人见人爱的帅哥。
function people(height, weight, character) {
this.height = 170;
this.weight = 80;
this.character = 'normal';
}
const xiaowang = people();
console.log(xiaowang.character);
function decorate(ctr) {
ctr.height = 180;
ctr.weight = 70;
ctr.character = 'handsome';
return ctr;
}
const wang = decorate(people);
console.log(wang.character);
代理
它在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。前端最常听到的代理就是 nginx 代理,它其实是代理的一个应用,把自身作为代理服务器将资源请求转发到终端服务器。在 JS 中比较典型的代理有图片懒加载,合并 http 请求,以及缓存计算乘积。
下面是一个图片懒加载的例子,我们加先加载默认图片,等真实图片加载完之后再替换默认图片。
const createImage = (function() {
const img = document.createElement('img');
document.body.appendChild(img);
return function(src) {
img.src = src;
};
})();
const proxyImage = function(fn) {
const image = new Image();
const defaultImg = 'https://rs.vip.miui.com/vip-resource/prod/mio/v136/static/media/lazyLoad.a10ffbd7.png';
return function(src) {
fn(defaultImg);
// 这里加一个延迟,可以更好的看到图片替换的过程。
setTimeout(function() {
image.src = src;
image.onload = function() {
fn(src);
};
}, 2000);
};
};
const proxy = proxyImage(createImage);
proxy('https://pic1.zhimg.com/80/v2-ec33fcec249a9cabab61b14436432bf0_r.jpg');
外观
也叫门面模式,GoF上的定义是,外观模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。它和适配器模式类似,配器是做接口转换,解决的是原接口和目标接口不匹配的问题,而外观模式做接口整合,解决多接口带来的调用问题。
比如下面的获取用户信息的接口。
getUserBaseInfo() {
return API.getUserBaseInfo();
}
getUserPriority() {
return API.getUserPriority();
}
getUserCustomContent() {
return API.getUserCustomContent();
}
// 对外提供一个总的接口
async getUserInfo() {
const baseInfo = await getUserBaseInfo();
const priority = await getUserPriority();
const customContent = await getUserCustomContent();
return { ...baseInfo, ...priority, ...customContent };
}
const userInfo = getUserInfo();
桥接
在《设计模式》中解释为将抽象和实现解耦,让它们可以独立变化。JS中天生就带了这个隐形的模式,一个方法一般会调用多个其他方法,这种将实现抽象出去,就是桥接模式。这里我就不贴代码了。
组合
不是我们平时说的组合关系。它规定了数据类型必须是树型结构,并且表示“部分-整体”的层次结构,是用来处理单个对象和组合对象之间的关系。
我们通过一个扫描文件的例子来说明。
class Folder {
constructor(name) {
this.name = name;
this.files = [];
}
add(file) {
this.files.push(file);
}
scan() {
console.log('开始扫描文件夹: ' + this.name);
this.files.forEach(file => {
file.scan();
})
}
};
class File {
constructor(name) {
this.name = name;
}
add(file) {
throw new Error('不能往文件里添加东西,' + file);
}
scan() {
console.log('开始扫描文件: ' + this.name);
}
}
const rootFolder = new Folder('根目录');
const folder1 = new Folder('一级目录');
const folder2 = new Folder('二级目录');
const file = new File('根文件');
const file1 = new File('一级文件');
const file2 = new File('二级文件');
const file3 = new File('也是二级文件');
rootFolder.add(folder1);
folder1.add(folder2);
rootFolder.add(file);
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
rootFolder.scan();
享元
应用于大量相似对象的系统。一般是借用工厂模式,新建一个对象,然后其他对象共享这个工厂对象,避免新建对象。享元模式是一种用时间换空间的优化模式,避免性能损耗。
享元模式的代码比较好理解,因为衣服的型号就那么几种,我们可以通过身高判断衣服类型(忽略贾玲和宋一茜),所以衣服类型就可以作为一个共享对象。
const selectClothes = (function() {
const clothesType = {
160: 's',
170: 'l',
175: 'xl',
180: 'xxl',
181: 'xxxl'
};
return function(height) {
if (height < 170) {
return clothesType[160];
} else if () {
// 后面的代码省略
}
}
});
class People {
constructor(height, weight) {
this.height = height;
this.weight = weight;
this.clothesType = '';
}
}
const people1 = new People(160, 100);
const people2 = new People(170, 150);
people1.clothesType = selectClothes(people1.height);
people2.clothesType = selectClothes(people2.height);
行为型
策略
定义一系列的算法,把它们一个个封装起来,并且可以相互替换,这就是策略模式。要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点, 这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。
我直接使用《JavaScript 设计模式和开发实战》中的一个例子。这是一个计算不同绩效的人对应不同的奖金(奖金 = 工资 * 对应的绩效算法)。
const strategies = {
S: function(salary) {
return salary * 4;
},
A: function(salary) {
return salary * 3;
},
B: function(salary) {
return salary * 2
}
}
const calculateBonus = function(level, salary) {
return strategies[level][salary];
}
const staff1 = calculateBonus('S', 10000);
const staff2 = calculateBonus('A', 20000);
模板
将公共的代码抽成一个抽象类,子类继承抽象类,并重写相应的方法。模板方法模式是为数不多的基于继承的设计模式。
因为JS中无法实现抽象类,我直接用TS实现这个例子。
如果你对TS还不了解,可以看这篇文章。
Peter Cheng:《TypeScript开发实战》总结
abstract class Beverage {
init() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
boilWater() {
console.log('把自来水煮沸');
}
abstract brew(): void
abstract pourInCup(): void
abstract addCondiments(): void
}
class Tea extends Beverage {
brew() {
console.log('用沸水浸泡茶叶');
}
pourInCup() {
console.log('把茶倒进杯子里');
}
addCondiments() {
console.log('加点糖');
}
}
class Coffee extends Beverage {
brew() {
console.log('用沸水浸泡咖啡');
}
pourInCup() {
console.log('把咖啡倒进杯子里');
}
addCondiments() {
console.log('加点牛奶');
}
}
const tea = new Tea();
tea.init();
const coffee = new Coffee();
coffee.init();
观察者
又称发布-订阅模式,它定义对象间的一种一对多的依赖关系。主要用于异步编程。JavaScript 本身也是一门基于事件驱动的语言,也利用了发布订阅模式。
下面是JS中自定义事件,它就是一个典型的观察者模式。
const msg = new Event('message');
window.addEventListener('message', function() {
console.log('我接收到了消息');
});
window.dispatchEvent(msg);
我们来手写一个观察者模式,下面这个模式比较简陋,边界处理很粗糙。
class Event {
constructor() {
this.events = [];
}
on(fn) {
this.events.push(fn);
}
emit(data) {
this.events.forEach(fn => {
fn(data);
})
}
// off方法我这里就不实现了,也比较简单
off () {}
}
const event = new Event();
event.on(function(data) {
console.log('我是第一个注册事件', data);
});
event.on(function(data) {
console.log('我是第二个注册事件', data);
});
event.emit('已发送');
迭代器
是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。分为内部迭代器和外部迭代器。
像我们大部分迭代器都是内部迭代器。比如 forEach,map ,filter 等。而外部迭代器,迭代的控制权在外部。
const Iterator = function(obj){
let current = 0;
const next = function(){
console.log(obj[current]);
current++;
};
const isDone = function(){
return current >= obj.length;
};
const getCurItem = function(){
return obj[current];
};
return {
next: next,
isDone: isDone,
getCurItem: getCurItem
}
};
const iterator = new Iterator([1, 2, 3]);
iterator.next(); // 1
iterator.next(); // 2
iterator.next(); // 3
中介者
定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。主要解决各个类之间关系复杂,且每个类都需要知道它要交互的类。这个时候就可以引入中介者,把脏活累活,耦合关系全放到中介者类中,我们其他类负责貌美如花。
下面是一个机场指挥塔的例子,它就是一个中介者,所有飞机想要获取航道信息都需要向指挥塔请求。
class PlaneCommandTower {
constructor(channel) {
this.channel = new Array(channel).fill(false);
}
enter(i) {
if (this.channel[i] === true || (i + 1 > this.channel.length)) {
throw new Error('这个航道已经被占用了');
}
console.log('航道预占成功');
this.channel[i] = true;
}
leave(i) {
this.channel[i] = false;
}
isBusy(i) {
return this.channel[i];
}
getChannelCnt() {
return this.channel.length;
}
}
class Plane {
constructor(commander) {
this.commander = commander;
this.channelCnt = this.commander.getChannelCnt();
}
land() {
let i = 0;
while(this.commander.isBusy(i)) {
++i;
if (i > this.channelCnt) {
break;
}
}
if (i < this.channelCnt) {
this.commander.enter(i);
} else {
console.log('航道已经被占满了');
return 'wait';
}
}
}
const commander = new PlaneCommandTower(3);
const plane1 = new Plane(commander);
const plane2 = new Plane(commander);
const plane3 = new Plane(commander);
const plane4 = new Plane(commander);
plane1.land();
plane2.land();
plane3.land();
plane4.land();
状态
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。举个例子,有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。
我们以灯的例子,扩展一下,比较高级的灯,不止开关两种状态,它有弱光,正常光,环保光,关四种状态。
class Light {
constructor() {
this.status = ['opened', 'closed'];
this.curStatus = -1;
}
setStatus(status) {
this.status = status;
}
press() {
this.curStatus = (this.curStatus + 1 === this.status.length) ? 1 : this.curStatus + 1;
console.log(this.status[this.curStatus]);
}
}
const light = new Light();
light.setStatus(['weak', 'common', 'environmental', 'closed']);
light.press(); // weak
light.press(); // common
light.press(); // environmental
light.press(); // closed
职责链
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。举个例子,如果早高峰能顺利挤上公交车的话,那么估计这一天都会过得很开心。因为公交车上人 实在太多了,经常上车后却找不到售票员在哪,所以只好把两块钱硬币往前面递。除非 你运气够好,站在你前面的第一个人就是售票员,否则,你的硬币通常要在 N 个人手上 传递,才能最终到达售票员的手里。
在JS中,无论是作用域链、原型链,还是 DOM 节点中的事件冒泡,我们都能从中找到职责链模式的影子。
假设我们负责一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用 户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金 的用户只能进入普通购买模式。
class Chain {
constructor(fn) {
this.fn = fn;
this.successor = null;
}
setNextSuccessor(successor) {
return this.successor = successor;
}
passRequest() {
const ret = this.fn.apply(this, arguments);
if (ret === 'nextSuccessor'){
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
}
}
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500);
chainOrder500.passRequest(2, true, 500);
chainOrder500.passRequest(3, true, 500);
chainOrder500.passRequest(1, false, 0);
命令
命令模式中的命令指的是一个执行某些特定事情的指令。命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。
下面通过一个按钮的点击事件,触发页面的刷新事件。
const button1 = document.createElement('button');
document.body.appendChild(button1);
const setCommand = function(button, func) {
button.onclick = function() {
func();
};
};
const MenuBar = {
refresh: function() {
console.log("刷新菜单界面");
}
};
const RefreshMenuBarCommand = function(receiver) {
return function() {
receiver.refresh();
};
};
const refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);
button1.click();
访问者
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。其次,必须定义一个访问者类,并且内部有visit方法。元素的执行算法实现accept方法,而内部通常都是visitor.visit(this);
function Visitor() {
this.visit = function (v) {
console.log('the computer type is ' + v.type);
}
}
function ComputerTypeVisitor(type) {
this.type = type;
this.accept = function (visitor) {
visitor.visit(this);
}
}
const visitor = new ComputerTypeVisitor('dell');
visitor.accept(new Visitor()); // the computer type is dell
备忘录
定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样就可以将该对象恢复到原先保存的状态
const Page = function () {
let page = 1,
cache = {},
data;
return function (page) {
if (cache[page]) {
data = cache[page];
render(data);
} else {
Ajax.send('cgi.xx.com/xxx', function (data) {
cache[page] = data;
render(data);
});
}
}
}();
解释器
它是一种特殊的设计模式,它建立一个解释器,对于特定的计算机程序设计语言,用来解释预先定义的文法。通俗点,你通过函数名的定义就能知道程序即将要执行的过程。
function Context() {
let sum;
let list = [];
this.getSum = function () {
return sum;
}
this.setSum = function (_sum) {
sum = _sum;
}
this.add = function (eps) {
list.push(eps);
}
this.getList = function () {
return list;
}
}
function PlusExpression() {
this.interpret = function (context) {
let sum = context.getSum();
sum++;
context.setSum(sum);
}
}
function MinusExpression() {
this.interpret = function (context) {
let sum = context.getSum();
sum--;
context.setSum(sum);
}
}
const context = new Context();
context.setSum(20);
//运行加法三次
context.add(new PlusExpression());
context.add(new PlusExpression());
context.add(new PlusExpression());
//运行减法两次
context.add(new MinusExpression());
context.add(new MinusExpression());
const list = context.getList();
for (let i = 0; i < list.length; i++) {
const expression = list[i];
expression.interpret(context);
}
console.log("打印输出结果:" + context.getSum()); // 打印输出结果:21
结束语
文章中的内容如果有错误,请及时指出。毕竟将面向对象标准的设计模式迁移到JS中,可能会存在使用不当的情况。文章中的好几个例子都来自《JavaScript 设计模式与开发实战》。等“设计模式之美-前端”篇章彻底结束,我会更新“数据结构和算法-前端”篇,敬请期待吧。
参考资料:《JavaScript 设计模式与开发实战》,《设计模式之美》专栏,《设计模式:可复用面向对象软件的基础》
如果你是前端小白,可以通过这篇文章找到前端之路,目测今年点赞能破1000。
