前端JS常用设计模式
话不多说,这里记录一些常见的设计模式,常看常新,也能提升JavaScript编程水平
一、设计原则
二、单例模式
单例模式的定义是,保证一个类仅有一个实例,并且要提供访问他的全局api
单例模式在前端是一种很常见的模式,一些对象我们往往就只需要一个,如VueX,React-redux等框架全局状态管理工具
1.Class语法简单实现
class Singleton { constructor (name) { this.name = name } // 静态方法 static getInstance (name) { if (!this.instance) { this.instance = new Singleton(name) } return this.instance } } let a = Singleton.getInstance('a1') let b = Singleton.getInstance('b2') console.log(a == b)
2.闭包结合new关键字
var instance function Singleton (name) { if (!instance) { return (instance = this) } return instance } let a = new Singleton('a1') let b = new Singleton('b1') console.log(a===b); // true
3.代理模式创建单例
// 定义代理 var CreateSingleton = (function () { let instance; // 接收一个普通类用于创建单例特性 return function(Singleton,name) { if(!instance) { return instance = new Singleton(name) } return instance } })() var Singleton = function(name) { this.name = name } let a = new CreateSingleton(Singleton,'a') let b = new CreateSingleton(Singleton, 'b') console.log(a===b); // true
- 代理只专注创建单例
- 让Singleton变成普通类
- 通过代理让Singleton普通类拥有单例特性
实际应用:如页面弹窗
惰性单例:
var createDiv = (function () { var instance return function () { if (!instance) { var div = document.createElement('div') div.innerHTML = '我是登录窗口' div.style.display = 'none' document.body.appendChild(div) return instance = div } return instance } })() button.addEventListener('click', function () { let div = createDiv() div.style.display = 'block' })
三、观察者模式
1.被观察者
// 被观察者类 class Subject { //未传值初始为空 constructor(state = "") { // 初始状态 this.state = state // 观察者方法队列 this.observsers = [] } // 设置自己的状态 setState(val) { // 告诉观察者目前改变成了什么状态 this.state = val; // 同时需要把自己身上的观察者方法全部触发 this.observsers.map(item => { // item是每一个观察者,每一个观察者是一个对象 item.callback(this.state); }) } // 添加观察者 addObserver(observer) { // 把观察者传递进来 this.observsers.push(observer) } // 删除观察者 removeObserver(observer) { // 过滤出来非当前观察者的观察者 this.observsers = this.observsers.filter(obs => obs !== observer); } }
2.观察者
// 观察者类 class Observer { // name需要观察的参数 // callback 观察的参数达到边界条件触发的事件 constructor(name, callback = () => { }) { this.name = name; this.callback = callback; } }
3.使用
初始数据
let obj = { name: "abc" }
创建观察者与被观察者
// 创建观察者 const ObserverName = new Observer('监听obj改变', () => { console.log('obj发生改变'); }) // 创建一个被观察者 const name = new Subject(obj.name) // 添加一个观察者 name.addObserver(ObserverName) //触发观察者方法 name.setState('123')
四、发布-订阅模式
模拟报纸的订阅与发布过程:
// 报社 class Publisher { constructor(name, channel) { this.name = name; this.channel = channel; } // 注册报纸 addTopic(topicName) { this.channel.addTopic(topicName); } // 推送报纸 publish(topicName) { this.channel.publish(topicName); } } // 订阅者 class Subscriber { constructor(name, channel) { this.name = name; this.channel = channel; } //订阅报纸 subscribe(topicName) { this.channel.subscribeTopic(topicName, this); } //取消订阅 unSubscribe(topicName) { this.channel.unSubscribeTopic(topicName, this); } //接收推送 update(topic) { console.log(`${topic}已经送到${this.name}家了`); } } // 第三方平台 class Channel { constructor() { this.topics = {}; } //报社在平台注册报纸 addTopic(topicName) { this.topics[topicName] = []; } //报社取消注册 removeTopic(topicName) { delete this.topics[topicName]; } //订阅者订阅报纸 subscribeTopic(topicName, sub) { if (this.topics[topicName]) { this.topics[topicName].push(sub); } } //订阅者取消订阅 unSubscribeTopic(topicName, sub) { this.topics[topicName].forEach((item, index) => { if (item === sub) { this.topics[topicName].splice(index, 1); } }); } //平台通知某个报纸下所有订阅者 publish(topicName) { this.topics[topicName].forEach((item) => { item.update(topicName); }); } }
这里的报社我们可以理解为发布者(Publisher)的角色,订报纸的读者理解为订阅者(Subscriber),第三方平台就是事件中心;报社在平台上注册某一类型的报纸,然后读者就可以在平台订阅这种报纸;三个类准备好了,我们来看下他们彼此如何进行联系:
var channel = new Channel(); var pub1 = new Publisher("报社1", channel); var pub2 = new Publisher("报社2", channel); pub1.addTopic("晨报1"); pub1.addTopic("晚报1"); pub2.addTopic("晨报2"); var sub1 = new Subscriber("小明", channel); var sub2 = new Subscriber("小红", channel); var sub3 = new Subscriber("小张", channel); sub1.subscribe("晨报1"); sub2.subscribe("晨报1"); sub2.subscribe("晨报2"); sub3.subscribe("晚报1"); sub3.subscribe("晨报2"); sub3.unSubscribe("晨报2"); pub1.publish("晨报1"); pub1.publish("晚报1"); pub2.publish("晨报2"); //晨报1已经送到小明家了 //晨报1已经送到小红家了 //晚报1已经送到小张家了 //晨报2已经送到小红家了
我们先定义了一个调度中心channel,然后分别定义了两个报社pub1、pub2,以及三个读者sub1、sub2和sub3;两家报社在平台注册了晨报1、晚报1和晨报2三种类型的报纸,三个读者各自订阅各家的报纸,也能取消订阅。
我们可以发现在发布者中并没有直接维护订阅者列表,而是注册了一个事件主题,这里的报纸类型相当于一个事件主题;订阅者订阅主题,发布者推送某个主题时,订阅该主题的所有读者都会被通知到;这样就避免了观察者模式无法进行过滤筛选的缺陷。
观察者模式与发布-订阅模式的区别
观察者模式是一种紧耦合的状态,发布/订阅模式相比于观察者模式多了一个中间媒介,因为这个中间媒介,发布者和订阅者的关联为松耦合
- 通知订阅者的方式不同
- 内部维护的内容不同
观察者模式把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的。发布/订阅模式中,发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知调度中心,由调度中心通知订阅者。
Vue的基本原理即运用发布-订阅模式,具体可查看手写vue2.x原理:https://gitee.com/younghxp/vue2-data-response
五、策略模式
策略模式的目的就是使算法的使用与算法分离开来
例子:不同的输入需要产生不同的输出,比如现在需要根据员工的等级来发最后的年终奖,这时候最简便的方法莫过于写一大堆 if···else···
语句了。就像这样:
var calculateBonus = function( level, salary ){ if ( level === 'S' ){ return salary * 4; } if ( level === 'A' ){ return salary * 3; } if ( level === 'B' ){ return salary * 2; } }; calculateBonus( 'B', 20000 ); // 输出:40000 calculateBonus( 'S', 6000 ); // 输出:24000
缺点明显:
- 整个calculateBouns函数太庞大了,包含了非常多的
if-else
语句,这些语句需要覆盖所有的语句。 - 违背了开放封闭原则。如果需要临时增加员工等级或者修改分配方式,那么只能直接去修改原来的代码,增加判断条件,这种侵入是很大的。
- 重用性低,这样的代码无法高效复用,只能无脑CV。
改写策略模式:
其实很好理解,Context就相当于一个中转站,接收不同的计算请求,然后把具体的计算任务转发给某一个具体策略类
1.定义一组策略类
let Bouns = function () { this.salary = null; this.strategy = null } Bouns.prototype.setSalary = function (salary) { // 设置员工的原始工资 this.salary = salary; } Bouns.prototype.setStrategy = function (strategy) { // 设置策略对象 this.strategy = strategy; } Bouns.prototype.getBouns = function () { return this.strategy.calculate(this.salary); }
2.实现中转站
let Bouns = function () { this.salary = null; this.strategy = null } Bouns.prototype.setSalary = function (salary) { // 设置员工的原始工资 this.salary = salary; } Bouns.prototype.setStrategy = function (strategy) { // 设置策略对象 this.strategy = strategy; } Bouns.prototype.getBouns = function () { return this.strategy.calculate(this.salary); }
3.实现策略模式年终奖计算
let bouns = new Bouns() bouns.setSalary(20000); bouns.setStrategy(new LevelS()) console.log(bouns.getBouns()) // 输出: 80000
JavaScript版本的策略模式
1.创建策略对象
let strategies = { 'S': function (salary) { return salary * 4 }, 'A': function (salary) { return salary * 3 }, 'B': function (salary) { return salary * 2 }, }
2.创建一个计算函数
function calculateBouns (level, salary) { return strategies[level](salary); } console.log(calculateBouns('S', 20000))
采用策略模式优点:
- 可以有效地避免多重条件选择语句
- 代码复用性高,避免了很多粘贴复制的操作。
- 策略模式提供了对开放封闭原则的支持,将算法独立封装在strategies中,使得它们易于切换,易于扩展。
六、代理模式
为一个对象提供一个代用品或占位符,以便控制对它的访问;
该模式场景需要三类角色,分别为使用者、目标对象和代理者,使用者的目的是直接访问目标对象,但却不能直接访问,而是要先通过代理者。因此该模式非常像明星代理人的场景。其特征为:
- 使用者无权访问目标对象;
- 中间加代理,通过代理做授权和控制
在js中常用到的是缓存代理和虚拟代理
1.虚拟代理
例子:图片懒加载场景
不使用代理模式:
// 不使用代理的预加载图片函数如下 var myImage = (function(){ var imgNode = document.createElement("img"); document.body.appendChild(imgNode); var img = new Image(); img.onload = function(){ imgNode.src = this.src; }; return { setSrc: function(src) { imgNode.src = "loading.gif"; img.src = src; } } })(); // 调用方式 myImage.setSrc("pic.png");
缺点:
- 代码耦合度比较大,一个函数内负责做了几件事情。未满足面向对象设计原则中单一职责原则;
- 当某个时候不需要图片预加载的时候,需要从myImage 函数内把代码删掉,这样代码耦合性太高。
使用代理模式:
var myImage = (function(){ var imgNode = document.createElement("img"); document.body.appendChild(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("loading.gif"); img.src = src; } } })(); // 调用方式 ProxyImage.setSrc("pic.png");
优点:
- myImage 函数只负责做一件事。创建img元素加入到页面中,其中的加载loading图片交给代理函数ProxyImage 去做。
- 加载成功以后,代理函数ProxyImage 会通知及执行myImage 函数的方法。
- 当以后不需要代理对象的话,我们直接可以调用本体对象的方法即可
2.缓存代理
var mult = function(){ var a = 1; for(var i = 0,ilen = arguments.length; i < ilen; i+=1) { a = a*arguments[i]; } return a; }; // 计算加法 var plus = function(){ var a = 0; for(var i = 0,ilen = arguments.length; i < ilen; i+=1) { a += arguments[i]; } return a; } // 代理函数 var proxyFunc = function(fn) { var cache = {}; // 缓存对象 return function(){ var args = Array.prototype.join.call(arguments,','); if(args in cache) { return cache[args]; // 使用缓存代理 } return cache[args] = fn.apply(this,arguments); } }; var proxyMult = proxyFunc(mult); console.log(proxyMult(1,2,3,4)); // 24 console.log(proxyMult(1,2,3,4)); // 缓存取 24 var proxyPlus = proxyFunc(plus); console.log(proxyPlus(1,2,3,4)); // 10 console.log(proxyPlus(1,2,3,4)); // 缓存取 10
实际应用:HTML元素事件代理;ES6 Proxy等
七、装饰器模式
装饰器,顾名思义,就是在原来方法的基础上去装饰一些针对特别场景所适用的方法,即添加一些新功能。因此其特征主要有两点:
- 为对象添加新功能;
- 不改变其原有的结构和功能,即原有功能还继续会用,且场景不会改变。
有点类似代理模式
class Circle { draw() { console.log('画一个圆形'); } } class Decorator { constructor(circle) { this.circle = circle; } draw() { this.circle.draw(); this.setRedBorder(circle); } setRedBorder(circle) { console.log('画一个红色边框'); } } let circle = new Circle(); let decorator = new Decorator(circle); decorator.draw(); //画一个圆形,画一个红色边框
装饰器模式,让对象更加稳定,且易于复用。而不稳定的功能,则可以在个性化定制时进行动态添加。
实际使用:ajax请求的拦截器等