JavaScript 设计模式
可复用面向对象软件的基础
大分类
- 创建型(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
结构型
适配器
适配器英文是 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';
return this;
}
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');
享元
应用于大量相似对象的系统。一般是借用工厂模式,新建一个对象,然后其他对象共享这个工厂对象,避免新建对象。享元模式是一种用时间换空间的优化模式,避免性能损耗。
享元模式的代码比较好理解,因为衣服的型号就那么几种,我们可以通过身高判断衣服类型(忽略贾玲和宋一茜),所以衣服类型就可以作为一个共享对象。
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 {
// 后面的代码省略
return clothesType[175];
}
}
})();
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);
观察者
又称发布-订阅模式,它定义对象间的一种一对多的依赖关系。主要用于异步编程。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
结束语
摘抄这篇文章内容用做学习,感谢作者大大,链接附上:设计模式之美-前端 - 知乎 (zhihu.com)