基础进阶-前端开发中常用的设计模式

1,045 阅读9分钟

设计模式:就是将常用的代码套路,归纳总结后系统的表达出来。
鉴于js语言特性以及前端实际开发的应用场景,笔者只在此总结几个经典的设计模式。

#工厂模式

工厂模式是用来创建对象最常用的设计模式,它不暴露创建对象的具体逻辑;而是将所有逻辑封装在函数中,通过这个函数批量化的创建对象。

哪些场景适合工厂模式?

  • 对象的构建十分复杂
  • 需要依赖具体环境创建不同实例
  • 处理大量具有相同属性的小对象

示例1

我们需要一个可以生成用户的构造函数,这个用户的实例可能是普通用户、商户、管理员。每种角色都有对应权限,只可访问有权限的页面。

  function createUser(type) {
      function User(option) {
        this.name = option.name
        this.viewPage = option.viewPage
      }
      switch (type) {
        case "user":
          return new User({ name: "user", viewPage: ["主页", "设置页"] })
          break;
        case "merchant":
          return new User({ name: "merchant", viewPage: ["主页", "设置页", "XXX页"] })
          break;
        case "admin":
          return new User({ name: "admin", viewPage: ["主页", "设置页", "XXX页", "XXXX页"] })
          break;
        default:
          throw new Error("参数错误")
          break;
      }
    }
    console.log(createUser("user"));

示例二

function createUser(role) {
  return new createUser.prototype[role]()
}
createUser.prototype.user = function () {
  this.name = "user";
  this.viewPage = ["主页", "设置页"]; 
}
createUser.prototype.admin = function () {
  this.name = "merchant";
  this.viewPage = ["主页", "设置页", "XXX页"]; 
}
createUser.prototype.merchant = function () {
  this.name = "merchant";
  this.viewPage = ["主页", "设置页", "XXX页", "XXXX页"]; 
}

#单例模式

单例模式可以确保一个特定的类最多只能存在一个实例,这意味着你第二次使用相同的类创建实例对象时,应该得到和第一次创建相同的对象,它的实现原理往往是通过闭包。

哪些场景适合单例模式?

  • 全局只需要唯一对象的,例如、弹窗、遮罩。包括ES6模块化和CommonJS模块化导出的对象也是唯一对象。

示例一

var singleUser = (function () {
    function User (name) {
      this.name = name;
    }
    var instance = null
    if(instance) return instance
    return instance = new User(Name)
})();

示例二

通过传入新的构造函数得到新的生成单例对象的构造函数  

var singleUser = function (fn) {
  var instance = null
  return function (args) {
    if(instance) return instance
    return instance = new fn(args)
  }
})

#适配器模式

适配器模式是将一个类(对象)的接口(方法或属性)转换成另外一个符合我们使用规范的类(对象)。它可已经将一些不兼容的接口放在一起正常的工作

哪些场景适合适配器模式

  • 当我们需要对接口的提供者和消费者进行兼容的时候,对旧代码的渐进式的改造以及对某些已有老接口的改造。

示例一

当前场景中,有一个110伏电压、三口插头的电源插座,而我们的设备需要的电源为220伏电压、两口插座。所以我们需要一个适配器帮我们完成适配

class Power{
  constructor() {
    this.serverVoltage = 100
    this.serverShape = "triangle"
  }
}
class Device{
  constructor() {
    this.consumerVoletage = 220
    this.consumerShape = "douple"
  }
  usePower(power){
    if(power) throw new Error("请接入电源")
    if(power.serverVoltage!==220||power.serverShape!=="douple) throw new Error("电源不符合规范“)
  }
}
class Adapter { 
  constructor() {
    this.serverVoltage = 220
    this.serverShape = "douple"
  }
  usePower(power){
    if(power) throw new Error("请接入电源")
    if(power.serverVoltage!==100||power.serverShape!=="triangle) throw new Error("电源不符合规范“)
    this.consumerVoletage = 100
    this.consumerShape = "triangle"  
  }
}
var myPower = new Power()
var myDevice = new Device()
var myAdapter = new Adapter()
myAdapter.usePwer(myPower)
myDevice.usePower(myAdapter)

#装饰器模式

装饰器模式是指向现有的对象添加新的新的功能,同时又不改变其结构。使多个不同的类或对象之间共享或拓展一些方法或行为。

哪些场景适合适配器模式

  • 装饰器可以极大的提高我们的开发效率,通过对非业务逻辑代码的封装快速完成可复用工作,例如React的高阶组件、Vue的TS语法,但是装饰器不要滥用 否则会让我们的代码混乱。

装饰器语法

  • 饰器装饰类时,装饰器传入的参数仅有一个,参数为被装饰的类的实例
  • 饰器装饰类实例的方法时,会获取三个参数:target(类的原型对象)、name(被装饰的方法名)、descriptor(属性描述符);属性描述符:value(属性值)、enumberable(是否可配置)、configurable(是否可配置)、writable(是否可重写)

示例一

使用js装饰器需要通过babel将这个高语语法进行转义变成浏览器能够识别的es2015语法

新建项目文件
/src、/src/index.html、/src/index.js

/src/index.js

function addSayHi(target){
  target.prototype.sayHi = function() {console.log("你好方法被调用“)}
}
@addSayHi
class Person {
  constructor(name){
    this.name = ame
  }
}
var zs = new Person("张三")
zs.sayHi()

安装babel依赖
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill

安装解析装饰器相关的插件
npm install --save-dev @babel/plugin-proposal-decorators
npm install --save-dev @babel/plugin-proposal-class-properties

配置babel
在项目根目录新建babel.config.json

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1",
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5",
      }
    ]
  ],
  //  用来解析装饰器和Class语法的插件
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose" : true }]
  ]
}

配置npm指令

"script":{
  "build":"babel index.js -o build.js"    //将index.js解析编译到build.js中
}

示例二

function useTool(target,name,descriptor){
  let oldValue = descriptor
  descriptor.value = function() {
    console.log("这是新方法")
    return oldValue.apply(this,arguments)
  }
}
class Tool {
  constructor() {}
  add(a,b){
    return a+b
  }
}

#代理模式

代理模式为一个对象提供另外一个对象,通过这个代理对象来控制对这个对象的访问。

proxy对象

proxy可以实现对一个对象的代理功能,proxy构造函数需要传入两个参数:target(需要被代理的源对象),handle(代理配置对象);handle具有两个拦截器方法,set(修改源对象属性时会被触发),get(访问源对象属性时会被触发)。set和get拦截器的都有两个形参:target(当前操作的源对象),key(当前操作的属性名)

哪些场景适合代理模式?

  • 调用对象需要被保护,我们通过代理对象来完成程序的逻辑操作。将代理对象与调用对象分离开降低系统上的耦合度。

示例1

var gege = {
  name:"哥哥",
  isHandsome:true
}
var proxy= new Proxy(gege,{
  get(target,key) {
    if(key=="isHandsome"&&!target[key]) {
       console.log("不,我们的哥哥很帅")
       return true
    }
    return target[key]
  },
  set(target,key,value) {
    if(key=="isHandsome"&&!value){
      console.log("修改成功,但是哥哥还是最帅的")
    }
      target[key] = value
   }
})

代理模式、装饰器模式、适配器模式的区别?

适配器模式提供了不同的新接口,通常用作接口的兼容处理 代理模式提供了一模一样上的接口,对行为进行拦截 装饰器模式直接访问原接口,对原接口进行改造。

#外观模式

外观模式就是对不同接口的封装,实现了所有接口的兼容处理。例如DOM绑定事件的兼容处理。

示例1

function addEvent(DOM,type,handle) {
  if(DOM.addEventListener) return DOM.addEventListener(type,handle,false)
  if(DOM.attachEvent) return DOM.attachEvent("on"+type,handle)
  DOM["on"+type] = handle 
}

#观察者模式

观察者模式定义了对象与对象间的一种依赖关系,只要当一个对象的状态发生变化时,所有依赖它的对象会得到通知并被自动更新。

示例1

//  创建发布者的类
class Publisher{
    // 实例的私有状态
    _state = null
    // 该发布者实例的订阅者列表
    substribers = null
    get state() {
      return this._state
    }
    // 当时state状态更新时,调用notify通知所有订阅者
    set state (value) {
      this._state = value
      this.notify({ key:"state",value})
    }
    notify(newState) {
      if(!this.substribers) return
      this.substribers.forEach(substriber =>substriber.update(newState));
    }
    // collect用来添加订阅者
    collect(substriber){
      if(!this.substribers) return this.substribers = [substriber]
      this.substribers.push(substriber)
    }
  }
  let id = 0
  //  创建订阅者的类
  class Substriber{
    publishers = null
    subId = ++id
    // 创建订阅者时需要传入被订阅的发布者
    constructor(publisher) {
      this.subscribe(publisher)
    }
    // subscribe用来添加订阅
    subscribe(publisher) {
      publisher.collect(this)
      if(!this.publishers) return this.publishers = [publisher]
      this.publishers.push(publisher)
    }
    // 订阅对象的状态被更改时执行的逻辑
    update(data) {
      console.log('发布新动态'+this.subId+"-接收到新属性",data);
    }
  }

#迭代器模式

迭代器模式 是指按照一中方法顺序的访问同一个聚合对象的各个元素,不用关心对象的内部结构,也可以按顺序访问各个元素。
迭代器分为内部迭代器和外部迭代器

内部迭代器示例

let arr = [1,2,3,4,5,6,7]
let each = function(arr,callback) {
  for(let index = 0;index<arr.length;index++){
    callback.call(null,i,arr[i])
  }
}
each(arr,function(i,value) {console.log("当前索引"+i+"当前值"+value)})

外部迭代器示例

class Iterator{
  constructor(list) {
    this.list = list
    this.index = 0
  }
  next() {
    // 每次迭代器调用next方法返回最新的值和 迭代完成的状态
    return {
      value:this.list[this.index++],
      done:this.index > this.list.length-1
    }
  }
}
var myIterator = new Iterator([1,2,3,,4])
myIterator.next()    

可迭代对象

ES6提供了迭代器接口,一些对象是默认可以使用的。例如:Array、Set、函数的arguments、NodeList、String。 以上对象都具备Symbol.iterator属性,调用该属性会返回外部迭代器对象,通过调用这个对象的next方法会迭代到下一步。
以上可迭代对象可以使用for...of迭代对象

Generator函数

Generator函数是ES6提供生产可迭代对象的函数,通过它可以实现同步代码写异步的逻辑。 generator生成器:他必须使用function* 来声明,函数的内部的必须使用yield来声明迭代的进度

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}
var res1 = foo(1)
var res2 = res1.next()

#状态模式

状态模式是将将主体对象和状态对象分离开,根据主体对象内部的状态分别委托不同的状态对象,每个状态对象都有不同的逻辑。

哪些场景适合状态模式?

  • 一个对象的行为取决于它的状态,并且他能在运行的时刻改变的他的行为
  • 一个操作中含有大量的if-else语句,并且这些分支语句都依赖于对象的状态

非状态模式

非状态模式 只能通过if判断不同的状态并据此执行不同逻辑。如果我们中间插入新的状态可能整个程序代码都有影响。

class Clame {
    this.state = "off"
    pressButton () {
    	if(this.state === "off") {
          console.log("切换到弱光模式")
          this.state = "weakLight"
        } else if(this.state === "weakLight") {
          console.log("切换到强光模式")
          this.state = "strongLight"
        } else if(this.state === "strongLight") {
          console.log("切换到关闭模式")
          this.state = "off"
        }
    }
}

状态模式

class Lamp {
  this.state =  FSM.offLight 
  pressButton() {
    this.state.trigger.call(this)
  }
}
/* FSM:有限状态机
 * 1、状态的总数是有限的
 * 2、任意时刻只会处于一个状态
 * 3、在某些情况下会从一种状态变为另外一种状态
*/
var FSM = {
  offLight:{
    trigger() {
      console.log("切换到弱光模式")
      this.state = FSM.weakLight
    }
  },
  weakLight:{
    trigger() {
      console.log("切换到强光模式")
      this.state = FSM.strongLight
    }
  },
  strongLight:{
    trigger() {
      console.log("切换到关灯模式")
      this.state = FSM.offLight
    }
  },
}

#享元模式

享元模式是将对象的公共部分抽离出来并做缓存处理,当创建大量对象可以节省内存开销。

哪些场景适合享元模式?

  • 页面中的表格,如果存在大量表格DOM,可以将他们抽离出来看上去是在滚动实际只改变当前单元格的内容。

示例

// IPHONE示例除了SN每个都是唯一的以外,型号、屏幕尺寸、内存是公共的部分
function Iphone (model,screen,memory,SN) {
  this.flyWeight = flyWeightFactory.get(model,screen,memory)
  this.SN = SN
}
function IphoneFlyWeight(model,screen,memory) {
  this.model = model
  this.screen = screen
  this.memory = memory
}

var flyWeightFactory = ()(
  //  通过闭包的函数来做一个函数处理
  var = iphones = {}
  return {
   get(model,screen,memory) {
     var key = model+screen+memory
     if(key) return iphones[key] = new IphoneFlyWeight(model,screen,memory)
     return iphones[key]
   }
  }
);

#备忘录模式

备忘录模式就是将上次操作的数据保存下来,一旦撤销到上次状态就把上次的数据覆盖回来。

示例

Vuex的浏览器"时光旅行"功能。

#中介者模式

中介者模式是封装的一层调度器,用来处理多对多的映射关系。例如MVC的controller