一,概念
设计模式是针对面向对象开发的一种问题解决方案。目的是以后开发的过程中,面对某些业务场景,可以顺利采用合适的设计模式,使代码更低耦合,易维护。
常见的设计模式有单例模式、工厂模式、策略模式、代理模式、中介者模式、装饰者模式等。
二,单例模式
原理:
单例模式是指保证一个类只有一个实例,且提供一个全局的访问点,在访问他的实例时,先判断当前实例是否已存在一个,如果存在则返回,如果不存在则先创建再返回。
使用场景:
很多场景都可以使用单例模式,例如模态框,这种一个页面只能同时存在一个的,也不需要多次创建的,无论点击多少次,模态框都只创建一次;封装websocket的链接池(类似于数据库连接池)
示例:
// 实现的方式有几种,可以使用原型、闭包、类+构造函数实现
// 定义类
class Person() {
// 构造函数
countructor(id, name) {
this.id = id; // 设置属性
this.name = name;
}
// 设置方法
getName() {
console.log(this.id, this.name);
}
}
// 使用var声明一个闭包,用于创建实例,这种也叫“代理实现单例模式”
var Instanse = (function () {
var instanse = null;
return function (id, name) {
if (!instanse) {
instanse = new Person(id, name);
}
return instanse;
}
})();
// 调用
var persopn = new Instanse('ins-1', '张三');
三,工厂模式
原理:
工厂模式是创建对象最常用的方式之一,主要核心是把创建各种对象的代码逻辑封装起来,外部调用时只传入参数即可,不必一个个实例化他们,不用关心内部逻辑。减少代码重复,也保证了代码的低耦合,降低开发者重复编写创建流程的代码错误等。
根据业务场景可以细分为普通工厂模式和抽象工厂模式。
普通工厂模式:工厂方法会根据传入参数的不同,生成对应的实例并返回
(1)静态工厂模式:较简单的,构造函数通过参数判断并return实例
(2)工厂方法模式:在静态工厂的基础下,把具体的实例创建封装到prototype中
抽象工厂模式:多一个抽象类(类似java中的抽象类),理解为工厂方法内部生成的是一个工厂
使用场景:
UI库创建组件;创建数据模型等。
他是最常用的创建对象方式。如果需要new一个实例(有构造函数的地方),就可以考虑使用厂模式;遇到几个实例有类似的部分,也可以使用工厂模式。大型项目的类似场景,可以使用抽象工厂模式,通常日常开发都是使用普通工厂模式。
示例:
静态工厂模式:
// 定义工厂
function Factory(type) {
// 定义创建构实例的造函数
function Person(type, style) {
this.type = type;
this.style = style;
}
let style;
switch (type) { // 根据类型生成不同的实例
case 'man':
style = {fat: true, beautiful: false};
return new Person('man', style);
break;
case 'woman':
style = {fat: false, beautiful: true};
return new Person('woman', style);
break;
case 'child':
style = {fat: false, beautiful: true};
return new Person('child', style);
break;
}
}
// 调用
let person = new Factory('man');
工厂方法模式:
// 定义工厂
function Factory(type) {
if(this instanceof Factory){
var f = new this[type](); // 实例化具体类型的对象
return f;
}else{
return new Factory(type);
}
}
// 在工厂方法中定义所有类型对象的构造函数
Factory.prototype = {
'man': function() {
this.type = 'man';
this.style = {fat: true, beautiful: false};
},
'man': function() {
this.type = 'woman';
this.style = {fat: false, beautiful: true};
},
'child': function() {
this.type = 'child';
this.style = {fat: false, beautiful: true};
}
}
// 调用
let person = new Factory('man');
抽象工厂模式:
因js没有抽象类的概念,因此只能通过代码模拟实现。具体的实现流程如下:
1,抽象工厂类
2,抽象类
3,具体类
4,实例化具体类
四,策略模式
原理:
策略模式就是将一系列的算法或者逻辑一个个的封装起来,将代码逻辑中不变的和变化的部分分开编码。不变的部分叫做策略类(封装了具体的算法和执行过程);变化的部分叫做环境类(用于分配使用哪个策略算法)。
应用场景:
特别常用的业务场景!通常是有多种状态或多种情况,对于代码的封装。例如用js原生实现需要很多if/else的场景;不同环境使用不同参数的场景等等
示例:
// 封装策略类,代替多个if else(每种情况就是封装的一个策略算法)
var moneyObj = {
"vvip": function(score) { // vvip,1积分 = 1块钱
return score / 1;
},
"vip": function(score) {
return score / 10; // vip,10积分 = 1块钱
},
"normal": function(score) { // 普通用户
return score / 100; // vip,100积分 = 1块钱
},
}
// 定义环境类,用于分配具体调用哪种策略
function getVipMoney(level, score) {
return moneyObj[level](score);
}
// 调用,计算当前会员卡积分可兑换多少钱
var leftMoney = getVipMoney('vip', 5300);
五,代理模式
原理:
顾名思义就是为一个对象提供代用品或者占位符,来控制对他的访问。最简单的实现就是ES6的Proxy对象,轻松实现了代理。代理模式的意义是用户不方便直接访问对象,或者不满足直接访问对象,就通过访问一个替身对象,而让替身对象去访问实际对象。
应用场景:
应用比较广泛,常见的前端框架例如axios请求;拦截器;缓存;懒加载;权限控制等。
缓存代理:为一个开销很大的运算进行提供暂时的存储,如果下次调用还是同样的参数,则直接返回。
虚拟代理:为一个开销很大的运算进行延迟加载,等用到这个对象的时候再去创建。
示例:
缓存代理:
// 实际对象
function add() {
let result;
for (let i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
// 代理对象(替身对象)
var proxyAdd = (function() {
var cache = {};
return function() {
var args = arguments.join(',');
if (args in cache) {
return cache[args]; // 从缓存获取
}
// 调用实际对象获取,计算的值存到缓存再返回
return (cache[args] = add.apply(this, arguments));
}
})();
// 调用
var result = proxyAdd(2, 3, 5);
虚拟代理:
// 实际对象,只暴露一个对外的setSrc接口
let img = (function(){
let node = document.createElement('img');
document.body.appendChild(imgNode);
return {
// 设置图片的路径
setSrc: function(src){
node.src = src;
}
}
})();
// 代理对象(替身对象),负责图片预加载功能
let proxyImg = (function(){
let img = new Image;
// 监听到图片加载完成后,再设置图片的url
img.onload = function(){
img.setSrc(this.src);
}
return {
// 图片未被真正加载好时,显示loading图
setSrc: function(src){
myImage.setSrc('./assect/loading.gif');
img.src = src;
}
}
})();
// 调用
proxyImg.setSrc('http://xxxx.jpg');
六,观察者模式
原理:
观察者模式就是观察者和被观察者之间的通信。定义了对象一对多的关系,当一个对象发生改变时,所有依赖他的对象都会收到通知,并进行相应的更新操作。
应用场景:
通常用于页面的事件监听器;回调数据变化时的更新和通知;发布订阅模型的实现等
示例:
// 观察者构造函数
function Observer(name) {
this.name = name;
}
// 观察者更新数据
Observer.Prototype.update = function(data) {
console.log('更新观察者' + this.name, data);
}
// 被观察者构造函数
function Subject() {
this.observers = []; // 观察者列表
}
// 添加观察者
Subject.prototype.add = function(observer) {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer);
}
}
// 删除观察者
Subject.prototype.delete = function(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
// 通知观察者
Subject.prototype.notice = function (data) {
this.observers.forEach(function (observer) {
observer.update(data);
});
}
// 调用
var sub = new Subject(); // 创建被观察者
var ob1 = new Observer('张三'); // 创建观察者
var ob2 = new Observer('李四'); // 创建观察者
sub.add(ob1); // 添加观察者,关联关系
sub.add(ob2); // 添加观察者,关联关系
sub.notice('更新名称'); // 通知观察者
七,发布-订阅模式
原理:
发布订阅模式与观察者模式类似,但是这里的发布者和订阅者没有依赖关系。发布订阅模式主要核心是发布者将消息分为不同的类别,发布到一个订阅中心;订阅者可以根据需求去订阅一个或多个类别。
与观察者模式的区别:
- 观察者模式中二者有依赖关系,被观察者发生变化通知观察者后,观察者进行更新;而发布订阅模式中二者没有依赖关系,订阅中心相当于代理方。
- 观察者模式通知消息属于同步通知(被观察者发送通知即观察者立即更新);而发布订阅模式通知属于异步通知,使用消息队列
- 观察者模式二者之间是紧耦合;而发布订阅模式二者之间是松耦合。
因此项目中根据不同的业务场景来确定需要使用的设计模式。
应用场景:
也可以用于页面事件的监听和处理;用于消息之间通信和传递;
示例:
// 订阅者构造函数
function Subscriber(name) {
this.name = name;
}
// 订阅者接收消息
Subscriber.prototype.reciver = function(data) {
console.log('订阅者' + this.name + '更新消息', data);
}
// 发布者构造函数
function Publisher() {
this.listenerMsgs = {}; // 订阅者消息订阅中心
}
// 订阅者订阅一个消息主题
Publisher.prototype.subscribe = function (topic, subscriber) {
if (!this.listenerMsgs[topic]) {
this.listenerMsgs[topic] = [];
}
this.listenerMsgs[topic].push(subscriber);
}
// 订阅者取消订阅一个消息主题
Publisher.prototype.unsubscribe = function (topic, subscriber) {
if (this.listenerMsgs[topic]) {
const index = this.listenerMsgs[topic].indexOf(subscriber);
if (index !== -1) {
this.listenerMsgs[topic].splice(index, 1);
}
}
}
// 发布消息
Publisher.prototype.publish = function(topic, data) {
this.listenerMsgs[topic].forEach((subscriber) => {
subscriber.reciver(data); // 调用订阅者接收消息
});
}
// 调用
var pub = new Publisher(); // 创建发布者
var sub1 = new Subscriber('张三'); // 创建订阅者1
var sub2 = new Subscriber('李四'); // 创建观察者2
pub.subscribe('新闻', sub1); // 订阅者1订阅了新闻主题
pub.subscribe('娱乐', sub1); // 订阅者1订阅了娱乐主题
pub.subscribe('娱乐', sub2); // 订阅者2订阅了娱乐主题
pub.publish('娱乐', '菜bd塌房了'); // 发布者发布消息
pub.publish('新闻', '俄乌最新进展'); // 发布者发布消息
八,装饰者模式
原理:
在不改变原始对象的前提下,在程序运行期间给对象动态添加方法,来满足现有的需求
应用场景:
即原理所述,在不修改原始对象的情况下,给其添加额外的功能;在运行时动态添加新的方法。
具体的业务场景比如需求迭代更新,原有的一系列方法需要修改时,在满足开放封闭原则(对外开放,对内修改封闭)的前提下,就可以使用此模式。(传统方式可能是修改原有核心逻辑,因此装饰者模式主要应对此场景)
示例:
// 原始的打印功能
function Print() {
this.log = function() {
console.log('A4大小灰色打印');
}
}
// 新需求-修改大小打印(定义新的装饰类)
function NewSizePrint(print) {
this.print = print;
this.log = function() {
this.print.log(); // 原始的打印功能
// 新打印功能
console.log('新大小');
}
}
// 调用
var print = new Print('xxx');
var newPrint = new NewSizePrint(print);
newPrint.print();
九,适配器模式
原理:
将老的接口转换成另外的一个接口,在适配器中实现对老接口的兼容,也可以称为转换器。
代理模式、装饰者模式、适配器模式的区别:
代理模式,主要是隔离,通过第三方去实现
装饰者模式,主要是扩展,对于现有功能的扩展实现
适配器模式,主要是兼容,对于各兼容场景封装一个转换器
应用场景:
第三方库等一些兼容性写法的场景;兼容不同浏览器的不同语法写法、jquery中的兼容性写法等。
示例:
// jquery的$('selector').on 的实现
function on(target, event, callback) {
if (target.addEventListener) { // 标准事件监听
target.addEventListener(event, callback);
} else if (target.attachEvent) { // IE低版本事件监听
target.attachEvent(event, callback)
} else { // 低版本浏览器事件监听
target[`on${event}`] = callback
}
}
十,迭代器模式
原理:
就是封装一个迭代器,为不同数据结构的集合提供统一的迭代接口,且不暴露内部结构。
应用场景:
如原理所述,封装一个不暴露内部结构的迭代器。
示例:
// 定义迭代器接口
function Iterator(list) {
this.list = list;
this.index = 0;
this.hasNext = function() {
if (this.index < this.list.length) {
return true;
}
return false;
}
this.next = function() {
if (this.hasNext()) {
return this.list[this.index++];
}
return null;
}
}
// 主体
function Container(list) {
// 生成遍历器
this.getIterator = function() {
return new Iterator(list);
}
}
// 调用
const arr = [1, 2, 3, 4];
const container = new Container(arr);
const iterator = container.getIterator();
while (iterator.hasNext()) {
console.info(iterator.next());
}