Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

点击上方 高级前端进阶,回复 “加群”

加入我们一起学习,天天进步

用自己通俗易懂的语言理解设计模式。

通过对每种设计模式的学习,更加加深了我对它的理解,也能在工作中考虑应用场合。

成文思路:分析每种设计模式思想、抽离出应用场景、对这些模式进行对比

此篇文章包含:修饰者模式(装饰器)、单例模式、工厂模式、订阅者模式、观察者模式、代理模式

将不变的部分和变化的部分隔开是每个设计模式的主题。

单例模式

也叫单体模式,核心思想是确保一个类只对应一个实例。

特点:

  • 只允许一个例存在,提供全局访问点,缓存初次创建的变量对象

  • 排除全局变量,防止全局变量被重写

  • 可全局访问

    // 工厂模式和 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

缺点:在于静态属性是能够被人为重写的,不过不会像全局变量那样被无意修改。

借助闭包

  1. 考虑重写构造函数:当对象第一次被创建以后,重写构造函数,在重写后的构造函数里面访问私有变量。
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 protptype1console.log(a1.pro2)//underfinedconsole.log(a2.pro1)//from protptype1console.log(a2.pro2)//underfinedconsole.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 protptype1console.log(a1.pro2)//from protptype2console.log(a2.pro1)//from protptype1console.log(a2.pro2)//from protptype2
  1. 利用立即执行函数来保持私有变量
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 protptype1console.log(a1.pro2)//from protptype2console.log(a2.pro1)//from protptype1console.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 = nullclass 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 个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用

工厂方法模式

工厂方法模式的本意是将实际创建对象的工作推迟到子类中,这样核心类就变成了抽象类。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

抽象工厂模式

抽象工厂其实是实现子类继承父类的方法。

抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

抽象工厂可以提供多个产品对象,而不是单一的产品对象。

参考 :JavaScript 设计模式与实践 -- 工厂模式

观察者模式或发布订阅模式

通常又被称为 发布 - 订阅者模式 或 消息机制,它**定义了对象间的一种一对多的依赖关系**,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。

最好理解的举例:公司里发布通知,让员工都知道。

工作中碰到以下几种,并进行分析。

用双向绑定来分析此模式:

双向绑定维护 4 个模块:observer 监听者、dep 订阅器、watcher 订阅者、compile 编译者

订阅器是手机订阅者(依赖),如果属性发生变化 observer 通知 dep,dep 通知 watcher 调用 update 函数(watcher 类中有 update 函数,并且将自己加入 dep)去更新数据,这是符合一对多的思想,也就是 observer 是一,watcher 是多。compile 解析指令,订阅数据变化,绑定更新函数。

理解下来,compile 类似于绑定员工的角色,把 watcher 加入一个集体,observer 通知它们执行。

用子组件与父组件通信分析此模式:

通过 emit 来发布消息,对订阅者 emit 来发布消息,对订阅者 emit 来发布消息,对订阅者 on 做统一处理。

emit 是发布订阅者,emit 是发布订阅者,emit 是发布订阅者,on 是监听执行

用 DOM 的事件绑定(比如 click)分析此模式:

addEventListener('click',()=>{}) 监听 click 事件,当点击 DOM 就是向订阅者发布这个消息。

点击 DOM 是发布,addEventListener 是监听执行

小结

通过分析平时碰到的这种模式,更好的理解一和多分别对应什么,也增加记忆。

一(发布事件):通知、广播发布

多(订阅事件,可能会做出不同的回应):观察者、监听者、订阅者

发布和订阅的意思都是变成多的角度(添加)

一是对应执行,多是收集

平时碰到的函数理解,写自己的函数也可以这么定义,有个全局 Events=[]:

publish/emit / 点击 / notify 订阅事件 调用函数 subscribe/on/addEventListener 监听 push 函数
unsubscribe/off/removeEventListener 删除

其实分析以上几种情况,发布订阅模式和观察者模式的思想差不多相同,但是也是有区别:

  • 观察者模式中需要观察者对象自己定义事件发生时的相应方法。

  • 发布订阅模式者在发布对象和订阅对象之中加了一个中介对象。我们不需要在乎发布者对象和订阅者对象的内部是什么,具体响应时间细节全部由中介对象实现。

订阅的东西用Map或者Object类型来存储。发布订阅模式,有个中介,也可以说是channel,但发现代码实现差不多,只不过发布订阅用来写包含有回调函数

实例参考:JavaScript 设计模式之观察者模式与发布订阅模式

juejin.cn/post/684490…

juejin.cn/post/684490…

装饰者模式

装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。

装饰者模式的适用场合:

  • 如果你需要为类增添特性或职责,可是从类派生子类的解决方法并不太现实的情况下,就应该使用装饰者模式。

  • 如果想为对象增添特性又不想改变使用该对象的代码的话,则可以采用装饰者模式。

  • 原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性。

装饰者模式除了可以应用在类上之外,还可以应用在函数上(其实这就是高阶函数)

我觉得可以是函数封装原函数。这样不改变原来

举例:为汽车添加反光灯、后视镜等这些配件

碰到的:对函数进行增强(节流函数 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 元素

参考:javascript 代理模式 (通俗易懂)

策略模式

策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换

策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。

策略模式的目的:将算法的使用算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成:

  • 第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。

  • 第二个部分是环境类 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 设计模式 -- 策略模式

建造者模式

应用场景:

  1. 创建时有很多必填参数需要验证。

  2. 创建时参数求值有先后顺序、相互依赖。

  3. 创建有很多步骤,全部成功才能创建对象。

class Programmer {  age: number  username: string  color: string  area: string  constructor(p) {    this.age = p.age    this.username = p.username    this.color = p.color    this.area = p.area  }  toString() {    console.log(this)  }}class Builder {  age: number  username: string  color: string  area: string  build() {    if (this.age && this.username && this.color && this.area) {      return new Programmer(this)    } else {      throw new Error('缺少信息')    }  }  setAge(age: number) {    if (age > 18 && age < 36) {      this.age = age      return this    } else {      throw new Error('年龄不合适')    }  }  setUsername(username: string) {    if (username !== '小明') {      this.username = username      return this    } else {      throw new Error('小明不合适')    }  }  setColor(color: string) {    if (color !== 'yellow') {      this.color = color      return this    } else {      throw new Error('yellow不合适')    }  }  setArea(area: string) {    this.area = area    return this  }}// testconst p = new Builder()  .setAge(20)  .setUsername('小红')  .setColor('red')  .setArea('hz')  .build()  .toString()

适配模式

举例:Target Adaptee Adapter ,Adapter 是需要继承 Target,并在里面调用 Adaptee 中的方法。

形象比拟:Target 是目标抽象类,实现插入插口的功能;Adaptee 是新的插头,包含了实现目标的方法;Adapter 是 implements Target,为了调用 Target 方法。这样,既能保留原功能(原函数不变),又能执行新功能(添加 Adaptee Adapter)

Target 是要实现的目标(比如打印日志,这是抽象的方法),如果不用适配模式,就需要重写函数,找到办法。

Adaptee 是适配者类,也就是插口,在适配器 Adapter 中,implements 来自于的 Target 方法(就是新方法,适配的方法,达到)中调用 Adaptee 中的方法。

Adaptee 在 Adapter 中调用,Adapter 最终是要调用 Adaptee 中需要的具体方法(也就是我们最终要达到目的使用的方法,此方法可在 Target 中的抽象方法中实现)。

场景:

  1. 以前开发的接口不满足需求,比如输出 log 存在本地盘改成存入云盘
  2. 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同
面试过程中,定义Target Adapter Adaptee分别实现的功能,再套用原理来实现。用ts实现,这样能使用interface和implements,更符合面向对象语言

优点

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。

  • 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。

  • 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,符合开闭原则

缺点

  • 过多地使用适配器,会让系统非常零乱,不易整体进行把握。

参考:TypeScript 设计模式之适配器模式

模板方法模式

很好理解,看它能很快理解

这九种常用的设计模式你掌握了吗

职责链模式

应用场景:

  1. 多个处理器 ABC 依次处理同一个请求,形成一个链条,当某个处理器能处理这个请求,就不会继续传递给后续处理器了。

  2. 过滤器 拦截器 处理器。

const order500 = function (orderType, pay, stock) {  if (orderType === 1 && pay === true) {    console.log("500 元定金预购, 得到 100 元优惠券");    return true;  } else {    return false;  }};const order200 = function (orderType, pay, stock) {  if (orderType === 2 && pay === true) {    console.log("200 元定金预购, 得到 50 元优惠券");    return true;  } else {    return false;  }};const orderCommon = function (orderType, pay, stock) {  if ((orderType === 3 || !pay) && stock > 0) {    console.log("普通购买, 无优惠券");    return true;  } else {    console.log("库存不够, 无法购买");    return false;  }};class chain {  fn: Function  nextFn: Function  constructor(fn: Function) {    this.fn = fn;    this.nextFn = null;  }  setNext(nextFn) {    this.nextFn = nextFn  }  init(...arguments) {    const result = this.fn(...arguments);    if (!result && this.nextFn) {      this.nextFn.init(...arguments); //这里看不懂    }  }}const order500New = new chain(order500);const order200New = new chain(order200);const orderCommonNew = new chain(orderCommon);order500New.setNext(order200New);order200New.setNext(orderCommonNew);order500New.init(3, true, 500); // 普通购买, 无优惠券链式

其他参考

juejin.cn/post/684490… JavaScript 中常见设计模式整理

juejin.cn/post/695342… *

juejin.cn/post/688138… *

juejin.cn/post/684490…

juejin.cn/post/684490…

关于本文

作者:hannie76327

https://juejin.cn/post/6953872475537014820

The End

欢迎自荐投稿,如果你觉得这篇内容对你挺有启发,我想请你帮我三个小忙:

1、点个 「在看」,让更多的人也能看到这篇内容

2、关注官网 https://muyiy.cn,让我们成为长期关系

3、关注公众号「高级前端进阶」,公众号后台回复 「加群」 ,加入我们一起学习并送你精心整理的高级前端面试题。

》》面试官都在用的题库,快来看看《《

“在看”吗?在看就点一下吧