用自己通俗易懂的语言理解设计模式。
通过对每种设计模式的学习,更加加深了我对它的理解,也能在工作中考虑应用场合。
成文思路:分析每种设计模式思想、抽离出应用场景、对这些模式进行对比
此篇文章包含:修饰者模式(装饰器)、单例模式、工厂模式、订阅者模式、观察者模式、代理模式
将不变的部分和变化的部分隔开是每个设计模式的主题。
单例模式
也叫单体模式,核心思想是确保一个类只对应一个实例。
特点:
-
只允许一个例存在,提供全局访问点,缓存初次创建的变量对象
-
排除全局变量,防止全局变量被重写
-
可全局访问
// 工厂模式和new模式实现单例模式
vue的安装插件属于单例模式
适用场景:适用于弹框的实现, 全局缓存,一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。
缺点:
防止全局变量被污染,看过多种写法,总结在一起,更能融会贯通
直接使用字面量(全局对象)
const person = {
name: '哈哈',
age: 18
}
复制代码
了解 const 语法的小伙伴都知道,这只喵是不能被重新赋值的,但是它里面的属性其实是可变的。
如果想要一个不可变的单例对象:
const person = {
name: '哈哈',
age: 18
}
Object.freeze(person);
复制代码
这样就不能新增或修改person的任何属性.
如果是在模块中使用,上面的写法并不会污染全局作用域,但是直接生成一个固定的对象缺少了一些灵活性
。
使用构造函数的静态属性
class写法
class A {
constructor () {
if (!A._singleton) {
A._singleton = this;
}
return A._singleton;
}
log (...args) {
console.log(...args);
}
}
var a1 = new A()
var a2= new A()
console.log(a1 === a2)//true
复制代码
构造函数写法
function A(name){
// 如果已存在对应的实例
if(typeof A._singleton === 'object'){
return A._singleton
}
//否则正常创建实例
this.name = name
// 缓存
A._singleton =this
return this
}
var a1 = new A()
var a2= new A()
console.log(a1 === a2)//true
复制代码
缺点:在于静态属性是能够被人为重写的,不过不会像全局变量那样被无意修改。
借助闭包
- 考虑重写构造函数:当对象第一次被创建以后,重写构造函数,在重写后的构造函数里面访问私有变量。
function A(name){
var instance = this
this.name = name
//重写构造函数
A = function (){
return instance
}
//重写构造函数之后,实际上原先的A指针对应的函数实际上还在内存中(因为instance变量还在被引用着),但是此时A指针已经指向了一个新的函数了
}
A.prototype.pro1 = "from protptype1"
var a1 = new A()
A.prototype.pro2 = "from protptype2"
var a2= new A()
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//underfined
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//underfined
console.log(a1.constructor ==== A) //false
复制代码
为了解决A指针指向新地址的问题,实现原型链继承
function A(name){
var instance = this
this.name = name
//重写构造函数
A = function (){
return instance
}
// 第一种写法,这里实际上实现了一次原型链继承,如果不想这样实现,也可以直接指向旧的原型
A.prototype = this
// 第二种写法,直接指向旧的原型
A.prototype = this.constructor.prototype
instance = new A()
// 调整构造函数指针,这里实际上实现了一次原型链继承,如果不想这样实现,也可以直接指向原来的原型
instance.constructor = A
return instance
}
A.prototype.pro1 = "from protptype1"
var a1 = new A()
A.prototype.pro2 = "from protptype2"
var a2= new A()
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2
复制代码
- 利用立即执行函数来保持私有变量
var A;
(function(name){
var instance;
A = function(name){
if(instance){
return instance
}
//赋值给私有变量
instance = this
//自身属性
this.name = name
}
}());
A.prototype.pro1 = "from protptype1"
var a1 = new A('a1')
A.prototype.pro2 = "from protptype2"
var a2 = new A('a2')
console.log(a1.name)
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2
复制代码
以上通过闭包的方式可以实现单例
代理实现单例模式
function singleton(name){
this.name = name
}
let proxySingleton = function(){
let instance = null
return function(name){
if(!instance){
instance = new singleton(name)
}
return instance
}
}()
let a1= new proxySingleton('a1')
let a2= new proxySingleton('a2')
console.log(123, a1===a2)
复制代码
工厂单例
let logger = null
class Logger {
log (...args) {
console.log(...args);
}
}
function createLogger() {
if (!logger) {
logger = new Logger();
}
return logger;
}
let a = new createLogger().log('12')
let b = new createLogger().log('121')
console.log(new createLogger(), a===b)
复制代码
根据理解,我自己喜欢用代理方式实现,更好理解。如果总结有错,欢迎指正。
参考:单例模式
工厂模式
不暴露创建对象的逻辑,封装在一个函数中。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。
简单工厂模式
简单工厂模式又叫静态工厂模式,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。
但是在函数内包含了所有对象的创建逻辑(构造函数)和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。当我们的对象不是上面的3个而是30个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
工厂方法模式
工厂方法模式的本意是将实际创建对象的工作推迟到子类中,这样核心类就变成了抽象类。
抽象工厂模式
抽象工厂其实是实现子类继承父类的方法
观察者模式或发布订阅模式
通常又被称为 发布-订阅者模式 或 消息机制,它**定义了对象间的一种一对多的依赖关系**
,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。
最好理解的举例:公司里发布通知,让员工都知道。
工作中碰到以下几种,并进行分析。
用双向绑定来分析此模式:
双向绑定维护4个模块:observer监听者、dep订阅器、watcher订阅者、compile编译者
订阅器是手机订阅者(依赖),如果属性发生变化observer通知dep,dep通知watcher调用update函数(watcher类中有update函数,并且将自己加入dep)去更新数据,这是符合一对多的思想,也就是observer是一,watcher是多。compile解析指令,订阅数据变化,绑定更新函数。
理解下来,compile类似于绑定员工的角色,把watcher加入一个集体,observer通知它们执行。
用子组件与父组件通信分析此模式:
通过 on 做统一处理。
on 是监听执行
用DOM的事件绑定(比如click)分析此模式:
addEventListener(‘click’,()=>{})监听click事件,当点击DOM就是向订阅者发布这个消息。
点击DOM是发布,addEventListener是监听执行
小结
通过分析平时碰到的这种模式,更好的理解一和多分别对应什么,也增加记忆。
一(发布事件):通知、广播发布
多(订阅事件,可能会做出不同的回应):观察者、监听者、订阅者
发布和订阅的意思都是变成多的角度(添加)
一是对应执行,多是收集
平时碰到的函数理解,写自己的函数也可以这么定义,有个全局Events=[]:
publish/emit/点击/notify 订阅事件
subscribe/on/addEventListener 执行
unsubscribe/off/removeEventListener 删除
其实分析以上几种情况,发布订阅模式和观察者模式的思想差不多相同,但是也是有区别:
- 观察者模式中需要观察者对象自己定义事件发生时的相应方法。
- 发布订阅模式者在发布对象和订阅对象之中加了一个中介对象。我们不需要在乎发布者对象和订阅者对象的内部是什么,具体响应时间细节全部由中介对象实现。
实例参考:JavaScript 设计模式之观察者模式与发布订阅模式
装饰者模式
装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。
装饰者模式的适用场合:
- 如果你需要为类增添特性或职责,可是从类派生子类的解决方法并不太现实的情况下,就应该使用装饰者模式。
- 如果想为对象增添特性又不想改变使用该对象的代码的话,则可以采用装饰者模式。
- 原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。
装饰者模式除了可以应用在类上之外,还可以应用在函数上(其实这就是高阶函数)
我觉得可以是函数封装原函数。这样不改变原来
举例:为汽车添加反光灯、后视镜等这些配件
碰到的:对函数进行增强(节流函数or防抖函数、缓存函数返回值、构造React高阶组件,为组件增加额外的功能)
参考: 使用装饰者模式做有趣的事情
代理模式
所谓的的代理模式就是为一个对象找一个替代对象,以便对原对象进行访问。
使用代理的原因是我们不愿意或者不想对原对象进行直接操作,我们使用代理就是让它帮原对象进行一系列的操作,等这些东西做完后告诉原对象就行了。就像我们生活的那些明星的助理经纪人一样。
原则:单一原则
常用的虚代理形式:保护代理、缓存代理、虚拟代理。
保护代理:明星委托助理或者经纪人所要干的事;
缓存代理:缓存代理就是将代理加缓存,更方便单一原则;
常用的虚拟代理:某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例:使用虚拟代理实现图片懒加载);
先占位,加载完,再加载所需图片
var imgFunc = (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() {
imgFunc.setSrc(this.src);
}
return {
setSrc: function(src) {
imgFunc.setSrc('./loading,gif');
img.src = src;
}
}
})();
proxyImage.setSrc('./pic.png');
复制代码
碰到的:Vue的Proxy、懒加载图片加占位符、冒泡点击DOM元素
策略模式
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换
。
策略模式的目的:将算法的使用算法的实现分离开来。
一个基于策略模式的程序至少由两部分组成:
- 第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。
- 第二个部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。
原则:开放-封闭原则
/*策略类 A B C就是可以替换使用的算法*/
var levelOBJ = {
"A": function(money) {
return money * 4;
},
"B" : function(money) {
return money * 3;
},
"C" : function(money) {
return money * 2;
}
};
/*环境类,维持对levelOBJ策略对象的引用,拥有执行算法的能力*/
var calculateBouns =function(level,money) {
return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000
复制代码
Context函数传入实际值,调用策略,可能同时调用多个策略,这样可以封装一函数循环调用策略,然后用Context函数调用此封装的函数
在工作中,很多if else,每种条件执行不同的算法,其实可以用到策略模式,比如验证表单
参考:js设计模式–策略模式
其他参考
juejin.cn/post/684490… JavaScript 中常见设计模式整理