JS 通用设计模式

1. 工厂模式

定义

用来创建对象的一种最常用的设计模式。不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂

function createUser(role) {
  function User(options) {
    this.name = options.name
    this.viewPage = options.viewPage
  }
  switch(role){
    case 'superAdmin':
      return new User({name:'超级管理员',viewPage:['首页','通讯录','发现页','应用数据','权限管理']})
      break
    case 'admin':
      return new User({name:'管理员',viewPage:['首页','通讯录','发现页','应用数据']})
      break
    case 'user':
      return new User({name:'普通用户',viewPage:['首页','通讯录','发现页']})
      break
    default:
      throw new Error('参数错误')
  }
}
createUser('admin')

如果要把多个构造函数生成实例的逻辑封装到某个工厂中,也可以将构造函数挂载到工厂函数的原型链上,或者工厂函数的静态方法中:

function createUser(role) {
  return new createUser.prototype[role]
}
createUser.prototype.superAdmin = function () {
  this.name = '超级管理员'
  this.viewPage = ['首页','通讯录','发现页','应用数据','权限管理']
}
createUser.prototype.admin = function () {
  this.name = '管理员'
  this.viewPage = ['首页','通讯录','发现页','应用数据']
}
createUser.prototype.user = function () {
  this.name = '普通用户'
  this.viewPage = ['首页','通讯录','发现页']
}

new createUser('admin')

以下几种情景下,开发者应该考虑使用工厂模式:

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

2. 单例模式

定义

保证一个特定类最多只能有一个实例,意味着第二次使用同一个类创建对象时,应得到和第一次创建对象完全相同

let SingleUser1 = (function () {
  let instance = null
  return function User() {
    if (instance) {
      return instance
    }
    return instance = this
  }
})()

对将一个构造函数单例化的逻辑可以进一步封装:

function User(name) {
  this.name = name
}

let singleton = function (fn) {
  let instance = null
  return function (args) {
    if (instance) {
      return instance
    }
    return instance = new fn(args)
  }
}

let singleUser = new singleton(User) 
let user1 = singleUser('zs') // { name : 'zs'}
let user2 = singleUser('ls') // { name : 'zs'}

凡是使用唯一对象的场景,都适用于单例模式,例如登录框,弹窗,遮罩。另外ES6和CommonJS模块化语法中导出的对象也是单例

export default new Vuex.Store({/**/})

例子:生成遮罩

let createMask = singleton(function(){
  let mask = document.createElement('div')
  mask.style.background = 'red'
  mask.style.width = '100%'
  mask.style.height = '100%'
  mask.style.position = 'fixed'
  document.body.appendChild(mask)
  return mask
})

3. 适配器模式

将一个类(对象)的接口(方法或属性)转化成用户希望的另外一个接口(方法或属性),适配器模式使得原本由于接口不兼容而不能一起工作的那些类(对象)可以正常工作

示例:

对旧的ajax方法进行迁移改造,由于历史原因,无法一次性移除所有的旧代码,因此需要使用适配模式对原代码进行兼容:

$ = {
  ajax(options) {
    let { method, url } = options
    let axiosOptions = {method, url}
    let dataProp = method === 'get' ? 'params' : 'data'
    axiosOptions[dataProp] = options.data
    return axios(axiosOptions).then((res) => {
      options.success && options.success(res.data,res.status,res.request)
    }).catch((err) => {
      options.error && options.error(err)
    })
  }
}

4. 装饰器模式

允许向一个现有的对象添加新的功能,同时又不改变其结构。例如手机壳,他并没有改变我们手机原有的功能,比如打电话,听音乐什么的。但却为手机提供了新的功能:防磨防摔等

简单实现

function Phone() {

}
Phone.prototype.makeCall = function () {
  console.log('拨通电话')
}

function decorate(target) {
  target.prototype.code = function () {
    console.log('写代码')
  }
  return target
}

Phone = decorate(Phone)
const phone = new Phone()

使用装饰器语法改造上面的例子:

function code (target) {
  target.prototype.code = function () {
    console.log('写代码')
  }
}

@code
class Phone {
  makeCall () {
    console.log('打电话')
  }
}

const phone = new Phone()

装饰器不光可以装饰类,还可以装饰方法:

class Math {
  @log
  add(a, b) {
    return a + b
  }
}

function log(target, name, descriptor) {
  // 此时target是 Math.prototype , name 是方法名,即'add' 
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // }
  var oldValue = descriptor.value
  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments)
    return oldValue.apply(this, arguments)
  }

  return descriptor
}

const math = new Math()
//现在调用add方法,则会触发log功能
math.add(2, 4)

5. 代理模式

为对象提供另一个代理对象以控制对这个对象的访问。

使用代理的原因是我们不想对原对象进行直接操作,而是通过一个“中间人”来传达操作。生活中有许多代理的例子,比如访问某个网站,不想直接访问,通过中间的一台服务器来转发请求,这台服务器就是代理服务器。又比如明星,普通人无法直接联系他们,而是通过经纪人进行联系。

简单实现

let star = {
  name:'zs',
  age:21,
  height:170,
  bottomPrice:100000,
  announcements:[]
}
let proxy = new Proxy(star,{
  get:function (target,key) {
    if (key === 'height') {
      return target.height + 10
    } else if(key === 'announcements') {
      return new Proxy(target.announcements,{
        set:function (target,key,value) {
          if(key !== 'length' && target.length === 3){
            console.log('不好意思,今年通告满了')
            return true
          }
          target[key] = value
          return true
        }
      })
    } else {
      return target[key]
    }
  },
  set:function (target, key, value,) {
    if (key === 'price') {
      if (value > target.bottomPrice * 1.5) {
        console.log('成交');
        target.price = value
      } else if (value > target.bottomPrice) {
        console.log('咱们再商量商量')
      } else {
        throw new Error('下次说吧')
      }
    }
  }
})

proxy.announcements.push('爸爸去哪儿')
proxy.announcements.push('中国好声音')
proxy.announcements.push('奇葩说')
proxy.announcements.push('快乐大本营')
proxy.price = 160000
proxy.price = 120000
proxy.price = 9000

应用:dom事件代理 。Vue源码

注意区分适配器模式(Adapter),装饰器模式(Decorator),代理模式(Proxy):

适配器模式提供不同的新接口,通常用作接口转换的兼容处理

代理模式提供一模一样的新接口,对行为进行拦截

装饰器模式,直接访问原接口,直接对原接口进行功能上的增强

6. 外观模式

为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易

简单例子

function addEvent(dom, type, fn) {
  if (dom.addEventListener) {      // 支持DOM2级事件处理方法的浏览器
    dom.addEventListener(type, fn, false)
  } else if (dom.attachEvent) {    // 不支持DOM2级但支持attachEvent
    dom.attachEvent('on' + type, fn)
  } else {
    dom['on' + type] = fn      // 都不支持的浏览器
  }
}

const myInput = document.getElementById('myinput')
addEvent(myInput, 'click', function() {console.log('绑定 click 事件')})

注意区分工厂模式外观模式

工厂模式核心是对创建对象的逻辑进行封装。

外观模式核心是对不同的接口进行封装。

7. 观察者模式

通常又被称为 发布-订阅者模式 (Publisher/Subscribers):它定义了对象和对象间的一种依赖关系,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合

简单实现

class Publisher{
  _state = 0;
  subscribers = []

  get state(){
    return this._state
  }
  set state(value){
    this._state = value
    this.notify(value)
  }
  notify(value){
    this.subscribers.forEach(subscriber => subscriber.update(value))
  }
  collect(subscriber){
    this.subscribers.push(subscriber)
  }
}
let subId = 1
class Subscriber{
  publisher = null;
  id = subId++;
  subscribe(publisher){
    this.publisher = publisher
    publisher.collect(this)
  }
  update(value){
    console.log(`我是${this.id}号订阅者,收到发布者信息:${value}`);
  }
}
let publisher = new Publisher()
let subscriber1 = new Subscriber()
let subscriber2 = new Subscriber()
subscriber1.subscribe(publisher)
subscriber2.subscribe(publisher)
publisher.state = 2

8. 迭代器模式

提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部结构

内部迭代器

在函数内部定义好迭代的规则,完全接手整个迭代的过程,外部只需一次初始调用

let arr = [1, 2, 3, 4, 5, 6, 7, 8]

let each = function(arr, callback){
  for(let i=0; i<arr.length; i++){
    callback.call(null, i, arr[i])    //把下标和元素当作参数传递给callback参数
  }
}

each(arr, function(i, value){
  console.log(i, value)
})

内部迭代器在调用时非常方便,但使用回调自由度有限,例如用户想在迭代途中暂停,或者迭代两个数组需要使用each嵌套

外部迭代器

指调用者显式地请求迭代下一个元素,虽然这样做会增加调用的复杂度,但也会增强迭代的操作灵活性

class Iterator {
  constructor(list) {
    this.list = list
    this.index = 0
  }
  next(){
    return {
      value:this.list[this.index++],
      done:this.index > this.list.length
    }
  }
}
let it = new Iterator([1,2,3])

ES6语法中提出了迭代器的概念,以下对象默认实现了ES6规定的迭代器接口

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

提供了调用之后能返回迭代器的接口的对象被称为可迭代对象。ES6中可以用for...of扩展运算符(...),数组解构,Array.from等语法迭代。

9. 状态模式

状态模式中是把对象的每种状态都进行封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化

状态模式在生活中很常见:十字路口的交通信号灯,一通电话有拨通,通话,挂断各个状态等等

假设有一个台灯,一开始是关闭状态,按下开关后会变成弱光状态,再次按下按钮会变成强光状态,再次按下按钮会变成关闭状态。如果不使用状态模式:

class Lamp {
  constructor() {
    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'
    }
  }
}

这样做存在有如下问题:

  • 每次新增或者修改状态名,都需要改动pressButton方法中的代码,这使得pressButton成为了一个非常不稳定的方法。并且这个方法的代码量会迅速膨胀,因为状态改变后的逻辑在实际开发中不仅仅只是例子里的打印
  • 状态之间的切换关系,不过是往pressButton方法里堆砌if、else语句,增加或者修改一个状态可能需要改变若干个操作,这使pressButton更加难以阅读和维护

如果使用状态模式,将状态封装成类,在JS中可以这样实现:

class Lamp {
  constructor() {
    this.offLightState = new OffLightState()
    this.weakLightState = new WeakLightState()
    this.strongLightState = new StrongLightState()
    this.state = this.offLightState
  }
  pressButton(){
    this.state.trigger.call(this)
  }
}
class OffLightState {
  trigger (){
    console.log( '弱光' )
    this.state = this.weakLightState
  }
}
class WeakLightState {
  trigger (){
    console.log( '强光' )
    this.state = this.strongLightState
  }
}
class StrongLightState {
  trigger (){
    console.log( '关灯' )
    this.state = this.offLightState
  }
}

如果状态不需要封装成类,可以用普通对象代替:

class Lamp {
  constructor() {
    this.state = FSM.offLightState
  }
  pressButton(){
    this.state.trigger.call(this)
  }
}
//有限状态机
//1. 状态总数是有限的
//2. 任一时刻,只处在一种状态之中
//3. 某种条件下,会从一种状态转变到另外一种状态
const FSM = {
  offLightState: {
    trigger() {
      console.log('弱光')
      this.state = FSM.weakLightState
    }
  },
  weakLightState: {
    trigger(){
      console.log('强光')
      this.state = FSM.strongLightState
    }
  },
  strongLightState:{
    trigger(){
      console.log('关灯');
      this.state = FSM.offLightState
    }
  }
}

以下这些情况中,我们应该考虑状态模式:

  1. 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
  2. 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态。

示例:

业务代码:tab栏,货物物流状态,音乐播放器的循环模式,游戏中各种状态等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值