JavaScript設(shè)計模式

1. 設(shè)計模式概述

  1. 簡介
    在面向?qū)ο筌浖O(shè)計過程中針對特定問題的簡潔而優(yōu)雅的解決方案。即設(shè)計模式是在某種場合下對某個問題的一種解決方案。
  2. 分類
    設(shè)計模式被劃分為創(chuàng)建型模式、結(jié)構(gòu)型模式以及行為型模式。其中,創(chuàng)建型模式的目的就是封裝創(chuàng)建對象的變化,結(jié)構(gòu)型模式封裝的是對象之間的組合關(guān)系,行為型模式封裝的是對象的行為變化
    (1) 創(chuàng)建型
    工廠模式、單例模式、原型模式。
    (2) 結(jié)構(gòu)型
    適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。
    (3) 行為型
    策略模式、模板方法模式、觀察者模式、迭代器模式、職責(zé)鏈模式、命令模式、備忘錄模式、狀態(tài)模式、訪問者模式、中介者模式、解釋器模式。

2. 21種設(shè)計模式

2.1 工廠模式

  1. 介紹
    new創(chuàng)建對象的封裝。
  2. UML類圖
    UML類圖
  3. 使用場景

(1) jQuery中的$('div')

class jQuery{
   // ...
}
window.$ = function(selector) {
   return new jQuery(selector)
}

(2) React中的React.createElement

class Vnode(tag, attrs, children) {
   // ...
}
React.createElement = function(tag, attrs, children) {
    return new Vnode(tag, attrs, children)
}

(3) Vue異步組件

Vue.component('async-example', function(resolve, reject) {
  setTimeout(function() {
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

工廠方式創(chuàng)建對象與new創(chuàng)建對象相比,書寫簡便并且封裝性更好。

2.2 單例模式

  1. 介紹
    保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
    注釋:單例模式的核心是確保只有一個實例,并提供全局訪問。
  2. 代碼演示
class Single {
  login() {
    console.log('login')
  }
}

Single.getInstance = (function() {
  let instance
  return () => {
    if(!instance) {
      instance = new Single()
    }
    return instance
  }
})()

let obj1 = Single.getInstance()
obj1.login()
let obj2 = Single.getInstance()
obj1.login()

console.log(obj1 === obj2) //true

注釋:① Single類的使用者必須知道這是一個單例類,與以往通過new Single()的方式不同,這里必須使用Single.getInstance()來創(chuàng)建單例對象。② 該示例為惰性單例,在使用的時候才創(chuàng)建對象。

  1. JavaScript中的單例模式
    在傳統(tǒng)面向?qū)ο笳Z言中,單例對象從類中創(chuàng)建而來。JavaScript其實是一門無類語言,創(chuàng)建唯一對象并不需要先創(chuàng)建一個類。傳統(tǒng)的單例模式實現(xiàn)在JavaScript中并不適用。
const single = {
  a() {
    console.log('1')
  }
}
  1. 創(chuàng)建對象與單例模式分離
const getSingle = fn => {
  let result
  return () => result || (result = fn(arguments)) 
}

const createObj = () => new Object();

const createSingleObj = getSingle(createObj)
const s1 = createSingleObj()
const s2 = createSingleObj()
console.log(s1) //{}
console.log(s1 === s2) //true

注釋:創(chuàng)建對象與管理單例的職責(zé)被分布在兩個不同的方法中。

  1. 使用場景

(1) jQuery中只有一個$

if(window.jQuery) {
  return window.jQuery
} else {
  // 初始化...
}

(2) vuexredux中的store

  1. 項目應(yīng)用場景總結(jié)
    mainboard為單例,是否可以不聲明為類,直接字面量對象。
    getInstance獲取單例方法修改為惰性單例的寫法。

2.3 適配器模式

  1. 介紹
    舊接口格式和使用者不兼容,使用適配器轉(zhuǎn)換接口。
  2. UML類圖
    image.png
  3. 代碼演示
class Adaptee {
  specifiRequest() {
    return '德國標(biāo)準(zhǔn)插頭'
  }
}

class Target {
  constructor() {
    this.adaptee = new Adaptee()
  }

  request() {
    let info = this.adaptee.specifiRequest()
    return `${info} - 轉(zhuǎn)換器 - 中國標(biāo)準(zhǔn)插頭`
  }
 }

 let target = new Target()
 console.log(target.request()) 
  1. 使用場景

(1) 封裝舊接口

//適配jQuery中的$.ajax()
const $ = {
  ajax: function(options) {
    return ajax(options)
  }
}

(2) vue computed

  <div id="app">
    <p>順序: {{message}}</p>
    <p>逆序: {{reverseMessage}}</p>
  </div>
  <script src = "https://cdn.bootcss.com/vue/2.5.14/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        message: 'hello'
      },
      computed: {
          reverseMessage: function() {
            return this.message.split('').reverse().join('')
          }
        }
    })
  </script>

2.4 裝飾器模式

  1. 介紹
    裝飾器模式可以動態(tài)地給某個對象添加額外的職責(zé),而不會影響從這個類中派生的其他對象。
  2. 模式分析
    在傳統(tǒng)的面向?qū)ο笳Z言中,給對象添加功能常常使用繼承的方式。繼承的方式并不靈活,會導(dǎo)致超類和子類存在強(qiáng)耦合性,并且在完成一些功能復(fù)用的同時,有可能創(chuàng)建出大量的子類。
    裝飾器模式能夠在不改變對象自身的基礎(chǔ)上,在程序運行期間給對象動態(tài)地添加職責(zé)。與繼承相比,裝飾者是一種更輕便靈活的做法,這是一種“即用即付”的方式。
  3. UML類圖
    image.png
  4. 代碼演示
class Plane{
  fire() {
    console.log('發(fā)射普通子彈')
  }
}

class MissileDecorator {
  constructor(plane) {
    this.plane = plane
  }

  fire() {
    this.plane.fire()
    console.log('發(fā)射導(dǎo)彈')
  }
}

class AtomDecorator {
  constructor(plane) {
    this.plane = plane
  }

  fire() {
    this.plane.fire()
    console.log('發(fā)射原子彈')
  }
}

let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire() //發(fā)射普通子彈 發(fā)射導(dǎo)彈 發(fā)射原子彈
  1. beforeafter鉤子函數(shù)
Function.prototype.before = function(beforefn) {
  var _self = this
  return function() {
    beforefn.apply(this, arguments)
    return _self.apply(this, arguments)
  }
}

Function.prototype.after = function(afterfn) {
  var _self = this
  return function() {
    var ret = _self.apply(this, arguments)
    afterfn.apply(this, arguments)
    return ret
  }
}

 var fun = function() {
  console.log(1)
 }

 var beforeFun = fun.before(function() {
   console.log(0)
 })
 beforeFun() //0 1 


 var afterFun = fun.after(function() {
   console.log(2)
 }).after(function() {
   console.log(3)
 })
 afterFun() //1 2 3
  1. ES7中的裝飾器
    (1) 配置環(huán)境
    ① 安裝插件
    ? design npm i babel-plugin-transform-decorators-legacy --save-dev
    ② 修改webpack打包配置文件
module.exports = {
 //...
  module: {
    rules: [{
    test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['babel-preset-env'],
          plugins: ['babel-plugin-transform-decorators-legacy']
        }
      }
    }]
  },
 //...
}

(2) 裝飾類

@testDec1
@testDec2(false)
class Demo {}

function testDec1( target) {
  target.isDec1 = true
}

function testDec2( bool ) {
  return function(target) {
    target.isDec2 = bool
  }
}

console.log(Demo.isDec1) //true
console.log(Demo.isDec2) //false

(3) 裝飾對象

function mixins(...list) {
  return function(target) {
    Object.assign(target.prototype, ...list)
  }
}

const Foo = {
  foo() {
    console.log('foo')
  }
}

@mixins(Foo)
class B {}

const b = new B()
b.foo() //foo

注釋:裝飾對象是裝飾類的一種特殊用法。
(4) 裝飾方法
① 設(shè)置方法只可執(zhí)行,不可修改

class Person {
  constructor() {
    this.first = 'A'
    this.last = 'B'
  }

  @readonly
  name() { return `${this.first} ${this.last}` }
}

function readonly(target, name, descriptor){
  // descriptor對象原來的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

const p = new Person()
//A B
console.log(p.name())  
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'
p.name = () => {} 

② 方法執(zhí)行添加日志

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

function log(target, name, descriptor) {
  let oldValue = descriptor.value
  descriptor.value = function() {
    console.log(`calling ${name} wich`, arguments)
    return oldValue.apply(this, arguments)
  }
  return descriptor
}

const math = new Math()
const result = math.add(2, 4)
console.log(result)
  1. core-decorators 第三方庫
    提供常用的裝飾器。
import { readonly, deprecate } from 'core-decorators'
class Person {
  constructor() {
    this.first = 'A'
    this.last = 'B'
  }

  @readonly
  name() { 
     return `${this.first} ${this.last}` 
  }

  @deprecate
  getName() {
    return `${this.first} ${this.last}`
  }
}

const p = new Person()
console.log(p.name()) 
//A B 
console.log(p.getName()) 
//DEPRECATION Person#getName: This function will be removed in future versions.
//A B 
p.name = () => {} 
//Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Person>'
  1. 項目應(yīng)用場景總結(jié)
    app監(jiān)控功能
    titleWidgetModel裝飾類
    ③ 使用裝飾類的方式替換類中的getInstance獲取單例的方法。

2.5 代理模式

  1. 介紹
    使用者無權(quán)訪問目標(biāo)對象,通過代理做授權(quán)和控制。
  2. UML類圖
    UML類圖
  3. 代碼演示
class RealImg {
  constructor(fileName) {
    this.fileName = fileName
    this.loadFromDisk()
  }

  display() {
    console.log(`display... ${this.fileName}`)
  }

  loadFromDisk() {
    console.log(`loading... ${this.fileName}`)
  }
}

class ProxyImg {
  constructor(fileName) {
    this.realImg = new RealImg(fileName)
  }

  display() {
    this.realImg.display()
  }
}

const proxyImg = new ProxyImg('1.png')
proxyImg.display()

注釋:代理和本體接口必須一致,在任何使用本體的地方都可以替換成使用代理。

  1. 使用場景

(1) 事件代理

var ul = document.getElementById('ul')
ul.addEventListener('click', function(e) {
  var target = e.target;
  if(target.nodeName === 'LI') {
    alert(target.innerHTML)
  }
})

(2) jQuery中的$.proxy

$('ul').click(function() {
  var self = this
  setTimeout(function() {
     $(self).css('background', 'red')
  }, 1000) 
})

//使用$.proxy保存this
$('ul').click(function() {
  setTimeout($.proxy(function() {
     $(this).css('background', 'red')
  }, this), 1000) 
})

(3) 圖片預(yù)加載

var myImg = (function() {
  var imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return function(src){
    imgNode.src = src
  }
})()

var proxyImg = (function() {
  var img = new Image()
  img.onload = function() {
    myImg(this.src)
  }

  return function(src) {
    myImg('loading.gif')
    img.src = src
  }
})()

proxyImg('1.png')

(4) 緩存代理

//計算乘積
const mult = function(){
  let a = 1
  for(let i = 0; i < arguments.length; i++) {
    a *= arguments[i]
  }
  return a
}

//緩存代理工廠
const createProxyFactory = fn => {
  let cache = {}
  return function(){
    const args = Array.prototype.join.call(arguments, ',')
    if(cache[args]) {
      return cache[args]
    }
    return cache[args] = fn.apply(this, arguments)
  }
}

var proxyMult = createProxyFactory(mult)
console.log(proxyMult(1,2,3,4)) //24
console.log(proxyMult(1,2,3,4)) //24

(5) ES6中的Proxy

//明星
let star = {
  name: '張XX',
  age: 25,
  phone: '13900001111'
}

//經(jīng)紀(jì)人
let agent = new Proxy(star, {
  get: function (target, key, receiver) {
    if(key === 'phone') {
      //返回經(jīng)紀(jì)人自己的電話
      return '13922225555'
    }
    if(key === 'price') {
      //明星不報價,經(jīng)紀(jì)人報價
      return 120000
    }
    return target[key]
  },
  set: function (target, key, value, receiver) {
    if(key === 'customPrice') {
      if(value < 100000) {
        throw new Error('價格太低')
      } else {
        target[key] = value
        return true
      }
    }
  }
})

console.log(agent.name) //張XX
console.log(agent.age) //25 
console.log(agent.phone) //13922225555
console.log(agent.price) //120000
agent.customPrice = 150000
console.log(star.customPrice) //150000
agent.customPrice = 90000 //Uncaught Error: 價格太低
  1. 與其他設(shè)計模式對比
    (1) 代理模式與適配器模式對比
    適配器模式:提供一個不同的接口。
    代理模式:提供一模一樣的接口。
    (2) 代理模式與裝飾器模式
    裝飾器模式:擴(kuò)展功能,原有功能不變且可直接使用。
    代理模式:使用原有功能,但是經(jīng)過限制和約束。
  2. 項目應(yīng)用場景總結(jié)
    ①【已完成】現(xiàn)有代碼中的this._groupsMap使用緩存代理替換。
    ②【已完成】 createZoomSelectComponent方法中使用緩存代理。

2.6 外觀模式

  1. 介紹
    為子系統(tǒng)中的一組接口提供了一個高層接口,使用者使用這個高層接口。
  2. 使用場景
function bindEvent(elem, type, selector, fn) {
  if(fn == null) {
    fn = selector
    selector = null
  }
  //...
}

// 調(diào)用
bindEvent(elem, 'click', '#div1', fn)
bindEvent(elem, 'click', fn)

2.7 觀察者模式

  1. 介紹
    觀察者模式又叫做發(fā)布-訂閱模式,它定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個對象的狀態(tài)發(fā)生變化時,所有依賴于它的對象都將得到通知。
  2. UML類圖
    image.png
  3. 代碼演示
class Subject {
  constructor() {
    this.state = 0
    this.observers = []
  }
  
  getState() {
    return this.state
  }

  setState(state) {
    this.state = state
    this.notifyAllObservers()
  }

  notifyAllObservers() {
    this.observers.forEach(observer => {
      observer.update(this.state)
    })
  }

  attach(observer) {
    this.observers.push(observer)
    observer.subject = this
  }
}

class Observer {
  constructor(name) {
    this.name = name
    this.subject = null
  }

  update(state) {
    console.log(`${this.name} update state to ${state}`)
  }
}

const sub = new Subject()
const obs1 = new Observer('obs1')
const obs2 = new Observer('obs2')
sub.attach(obs1)
sub.attach(obs2)

sub.setState(1)
sub.setState(2)
sub.setState(3)
  1. 使用場景

(1) DOM事件

document.body.addEventListener('click', function() {
  console.log(1)
}, false)

document.body.addEventListener('click', function() {
  console.log(2)
}, false)

document.body.click() //模擬用戶點擊

(2) Promise

(3) jQuerycallbacks

var callbacks = $.Callbacks()
callbacks.add(function(info) {
  console.log('fn1', info)
})
callbacks.add(function(info) {
  console.log('fn2', info)
})
callbacks.add(function(info) {
  console.log('fn3', info)
})

callbacks.fire('go')
callbacks.fire('fire')

(4) nodejs自定義事件
EventEmitter

const EventEmitter = require('events').EventEmitter

const emitter = new EventEmitter()
emitter.on('some', info => {
  console.log('fn1', info)
})

emitter.on('some', info => {
  console.log('fn2', info)
})
emitter.emit('some', 'fire1')

EventEmitter應(yīng)用

const EventEmitter = require('events').EventEmitter

class Dog extends EventEmitter {
  constructor(name) {
    super()
    this.name = name
  }
}

const simon = new Dog('simon')
simon.on('bark', function(voice) {
  console.log(this.name, voice)
})
setInterval(function() {
  simon.emit('bark', '汪')
}, 1000)

fs文件系統(tǒng)讀取文件字符數(shù)

const fs = require('fs')
const readStream = fs.createReadStream('./node_modules/accepts/index.js')
let length = 0
readStream.on('data', function(chunk) {
  let len = chunk.toString().length
  console.log('len', len) //len 5252
  length += len
})
readStream.on('end', function() {
  console.log('length', length) //length 5252
})

fs文件系統(tǒng)讀取文件行數(shù)

const fs = require('fs')
const readLine = require('readline')
let rl = readLine.createInterface({
  input: fs.createReadStream('./node_modules/accepts/index.js'),
})
let lineNum = 0
rl.on('line', function(line) {
  lineNum ++
})
rl.on('close', function() {
  console.log('lineNum', lineNum) //lineNum 238
})

2.8 迭代器模式

  1. 介紹
    提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內(nèi)部表示。
    注釋:object不是有序數(shù)據(jù)集合,ES6中的map是有序數(shù)據(jù)集合。
  2. UML類圖
    UML類圖
  3. 代碼演示
class Iterator {
  constructor(container) {
    this.list = container.list
    this.index = 0
  }

  next() {
    if(this.hasNext()) {
      return this.list[this.index++] 
    }
    return null
  }

  hasNext() {
    return this.index < this.list.length
  }
}

class Container {
  constructor(list) {
    this.list = list
  }

  getIterator() {
    return new Iterator(this)
  }
}

var arr = [1, 2, 3, 4, 5]
var container = new Container(arr)
var iterator = container.getIterator()
while(iterator.hasNext()) {
  console.log(iterator.next())
}
  1. 使用場景

(1) jQuery中的each方法

var arr = [1,2,3]
var nodeList = document.getElementsByTagName('li')
var $a = $('li')

function each(data) {
  var $data = $(data)
  $data.each(function(key, val) {
    console.log(key, val)
  })
}

each(arr)
each(nodeList)
each($a)

(2) 迭代器模式替換if...else

const getActiveUploadObj = () => {
  try{
    return new ActiveXObject('TXFTNActiveX.FTNUpload')
  } catch (e) {
    return false
  }
}

const getFlashUploadObj = () => {
  if(supportFlash()) {
    const str = '<object type="application/x-shockwave-flash"></object>'
    return $(str).appendTo($('body'))
  }
}

const getFormUploadObj = () => {
  const str = '<input name="file" type="file" class="ui-file" />'
  return $(str).appendTo($('body'))
}

const interatorUploadObj = function(){
  for(let i = 0, fn; fn=arguments[i++];) {
    var uploadObj = fn()
    if(uploadObj !== false) {
      return uploadObj
    }
  } 
}

const uploadObj = interatorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)

(3) ES6中的Iterator
ES6語法中,有序數(shù)據(jù)集合的數(shù)據(jù)類型有多個。例如:Array、MapSet、StringTypedArray等。需要有一個統(tǒng)一的遍歷接口來遍歷所有數(shù)據(jù)類型。以上數(shù)據(jù)類型都有一個[Symbol.iterator]屬性。屬性值是函數(shù),執(zhí)行函數(shù)返回一個迭代器。此迭代器有next()方法可順序迭代子元素。

Array.prototype[Symbol.iterator]
? values() { [native code] }
Array.prototype[Symbol.iterator]()
Array Iterator {}
Array.prototype[Symbol.iterator]().next()
{value: undefined, done: true}

Iterator使用示例

function each(data) {
  let iterator = data[Symbol.iterator]()

  let item = { done: false }
  while(!item.done) {
    item = iterator.next()
    if(!item.done) {
      console.log(item.value)
    }
  }
}

var map = new Map()
map.set(1, 'a')
map.set(2, 'b')
var arr = [1,2,3]
var str = 'abc'
each(arr)
each(str)
each(map)

ES6中的for...of語法是對Iterator的封裝

function each(data) {
  for(let item of data) {
    console.log(item)
  }
}

var map = new Map()
map.set(1, 'a')
map.set(2, 'b')
var arr = [1,2,3]
var str = 'abc'
each(arr)
each(str)
each(map)
  1. 項目應(yīng)用場景總結(jié)
    ①【已完成】 createZoomSelectComponent方法中使用迭代器模式替換if...else...。

2.9 狀態(tài)模式

  1. 介紹
    將狀態(tài)封裝成獨立的類,并將請求委托給當(dāng)前的狀態(tài)對象,當(dāng)對象的內(nèi)部狀態(tài)改變時,會帶來不同的行為變化。
    注釋:狀態(tài)模式的關(guān)鍵是把事物的每種狀態(tài)都封裝成單獨的類,跟此種狀態(tài)有關(guān)的行為都被封裝在這個類的內(nèi)部。
  2. UML類圖
    image.png
  3. 代碼演示
class OffLightState {
  constructor(light) {
    this.light = light
  }

  switch() {
    console.log('微光') //offLightState對應(yīng)的行為
    this.light.setState(this.light.weakLightState) //切換狀態(tài)到weakLightState
  }
}

class WeakLightState {
  constructor(light) {
    this.light = light
  }

  switch() {
    console.log('強(qiáng)光')
    this.light.setState(this.light.strongLightState)
  }
}

class StrongLightState {
  constructor(light) {
    this.light = light
  }

  switch() {
    console.log('關(guān)燈')
    this.light.setState(this.light.offLightState)
  }
}

class Light {
  constructor() {
    this.offLightState = new OffLightState(this)
    this.weakLightState = new WeakLightState(this)
    this.strongLightState = new StrongLightState(this)
  }

  init() {
    this.currState = this.offLightState
  }

  setState(newState) {
    this.currState = newState
  }

  press() {
    this.currState.switch()
  }
}

const light = new Light()
light.init()
light.press() //微光
light.press() //強(qiáng)光
light.press() //關(guān)燈

注釋:狀態(tài)的切換規(guī)律事先被完好定義在各個狀態(tài)類中。在Context中再也找不到任何一個跟狀態(tài)切換相關(guān)的條件分支語句。

  1. 使用場景
    (1) 有限狀態(tài)機(jī)
    有限個狀態(tài)之間切換。
    使用開源庫:javascript-state-machine
import StateMachine from 'javascript-state-machine'
import $ from 'jquery'

let fsm = new StateMachine({
  init: '收藏',
  transitions: [
    {
      name: 'doStore',
      form: '取消收藏',
      to: '收藏'
    },
    {
      name: 'deleteStore',
      form: '收藏',
      to: '取消收藏'
    }
  ],
  methods: {
    onDoStore: function() {
      alert('收藏成功') 
      updateText()
    },
    onDeleteStore: function() {
      alert('取消收藏成功') 
      updateText()      
    }
  }
})

let $btn = $('#btn')

$btn.click(function() {
  if(fsm.is('取消收藏')) {
    fsm.doStore()
  } else {
    fsm.deleteStore()
  }
})

// 更新按鈕文案 
function updateText() {
  $btn.text(fsm.state)
}

//初始化文案
updateText()

(2) Promise實現(xiàn)

import StateMachine from 'javascript-state-machine'

const fsm = new StateMachine({
  init: 'pending',
  transitions: [
    {
      name: 'resolve',
      from: 'pending',
      to: 'fullfilled'
    },
    {
      name: 'reject',
      from: 'pending',
      to: 'rejected'
    }
  ],
  methods: {
    onResolve(state, data){
      //state - 當(dāng)前狀態(tài)機(jī)實例; data - fsm.resolve(xxx) 傳遞的參數(shù)
      data.successList.forEach(fn => fn())
    },
    onReject(state, data){
      //state - 當(dāng)前狀態(tài)機(jī)實例; data - fsm.reject(xxx) 傳遞的參數(shù)
      data.failList.forEach(fn => fn())
    }
  }
})

class MyPromise {
  constructor(fn) {
    this.successList = []
    this.failList = []
    fn(() => {
      // resolve 函數(shù)
      fsm.resolve(this)
    }, () => {
      // reject 函數(shù)
      fsm.reject(this)
    })
  }

  then(successFn, failFn) {
    this.successList.push(successFn)
    this.failList.push(failFn)
  }
}

function loadImg(src) {
  const mp = new MyPromise((resolve, reject) => {
    const img = document.createElement('img')
    img.onload = () => resolve(img)
    img.onerror = () => reject()
    img.src = src
  })
  return mp
}

let src = 'https://www.baidu.com/img/bd_logo1.png'
let mp = loadImg(src)

mp.then(() => {
  console.log('success1')
}, () => {
  console.log('fail1')
})

mp.then(() => {
  console.log('success2')
}, () => {
  console.log('fail2')
})
  1. 與策略模式對比
    策略模式中各個策略類之間是平等又平行的,他們之間沒有任何聯(lián)系,客戶必須熟知這些策略類的所用,以便隨時主動切換算法;而在狀態(tài)模式中,狀態(tài)之間的切換早已被預(yù)先設(shè)定,“改變行為”這件事情發(fā)生在狀態(tài)模式內(nèi)部,客戶并不需要了解這些細(xì)節(jié)。
  2. 項目應(yīng)用場景總結(jié)
    ① 可放大組件的放大狀態(tài)。
    ② 可選中組件的選中狀態(tài)。
    ③ 參數(shù)面板展示隱藏狀態(tài)。
    ④ 導(dǎo)航欄和狀態(tài)欄的顯示隱藏狀態(tài)。

2.10 原型模式

JavaScript選擇了基于原型的面向?qū)ο笙到y(tǒng)。在原型編程的思想中,類并不是必須的,對象未必從類中創(chuàng)建而來,一個對象可以通過克隆另一個對象而得到。

2.10.1 使用克隆的原型模式

  1. 原型模式是用于創(chuàng)建對象的一種模式。我們不再關(guān)心對象的具體類型,而是找到一個對象,然后通過克隆來創(chuàng)建一個一模一樣的對象
  2. ES5中提供了Object.create方法,可以用來克隆對象。
let obj = {
  getName() {
    return this.first + '  ' + this.last
  },

  say() {
    alert('hello')
  }
}

let x = Object.create(obj)
x.first = 'A'
x.last = 'B'
alert(x.getName())
x.say()

在不支持Object.create方法的瀏覽器中,則可以使用以下代碼:

//兼容不支持瀏覽器
Object.create = Object.create || function(obj) {
  var F = function() {}
  F.prototype = obj
  return new F()
}
  1. 原型模式提供了一種便捷的方式去創(chuàng)建某個類型的對象,克隆只是創(chuàng)建這個對象的過程和手段。通過克隆,我們不再關(guān)心對象的具體類型。

2.10.2 JavaScript中的原型繼承

  1. 原型編程遵循以下基本規(guī)則
    ① 所有的數(shù)據(jù)都是對象。
    ② 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它。
    ③ 對象會記住它的原型。
    ④ 如果對象無法響應(yīng)某個請求,它會把這個請求委托給自己的原型。
  2. JavaScript中的根對象是Object.prototypeObject.prototype對象是一個空的對象。JavaScript中的每一個對象,實際上都是從Object.prototype對象克隆而來的。
var obj1 = new Object()
var obj2 = {}

console.log(Object.getPrototypeOf(obj1) === obj1.__proto__)  //true
console.log(Object.getPrototypeOf(obj1) === Object.prototype) //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype) //true

2.11 橋接模式

  1. 介紹
    把抽象化與實現(xiàn)化解耦,使兩者可以獨立變化。
  2. 代碼演示
class Color {
  constructor(name) {
    this.name = name
  }
}

class Shape {
  constructor(name, color) {
    this.name = name
    this.color = color
  }

  draw() {
    console.log(`${this.color.name} ${this.name}`)
  }
}

let red = new Color('red')
let circle = new Shape('circle', red)
circle.draw() //red circle

2.12 組合模式

  1. 介紹
    生成樹形結(jié)構(gòu),表示“整體-部分”關(guān)系。讓整體和部分具有一致的操作方式。
    注釋:組合模式最大的優(yōu)點在于可以一致地對待組合對象和基本對象??蛻舨恍枰喇?dāng)前處理的是宏任務(wù)還是普通任務(wù)。
  2. 特點
    ① 使用樹形方式創(chuàng)建對象的結(jié)構(gòu)。
    ② 把相同操作應(yīng)用在組合對象和單個對象上。
  3. 使用場景
    虛擬DOM中的vnode是這種形式。
  4. 組合模式注意事項
    ① 組合對象和葉對象是聚合關(guān)系而非父子關(guān)系。
    ② 組合對象和葉對象擁有相同的接口。
    ③ 對組合對象和葉對象的操作必須具有一致性。
  5. 項目應(yīng)用場景總結(jié)
    ① 布局與控件的關(guān)系使用組合模式。先創(chuàng)建整個表單的樹結(jié)構(gòu),再逐層遍歷,而不是生成單層布局-控件結(jié)構(gòu)。

2.13 享元模式

  1. 介紹
    享元模式是一種用于性能優(yōu)化的模式,享元模式的核心是運用共享技術(shù)來有效支持大量細(xì)粒度的對象。
  2. 代碼演示
class Model {
  constructor(sex) {
    this.sex = sex
    this.underwear = ''
  }

  setUnderwear(underwear) {
    this.underwear = underwear
  }

  takePhoto() {
    console.log(`sex: ${this.sex}; underwear: ${this.underwear}`)
  }
}

const maleModel = new Model('male')
const femaleModel = new Model('female')

for(let i = 1; i<= 50; i++) {
  maleModel.setUnderwear('underwear' + i)
  maleModel.takePhoto()
}

for(let j = 1; j<= 50; j++) {
  femaleModel.setUnderwear('underwear' + j)
  femaleModel.takePhoto()
}
  1. 內(nèi)部狀態(tài)與外部狀態(tài)
    享元模式要求將對象的屬性劃分為內(nèi)部狀態(tài)和外部狀態(tài)(狀態(tài)在這里通常指屬性)。
    ① 內(nèi)部狀態(tài)存儲于對象內(nèi)部。
    ② 內(nèi)部狀態(tài)可以被一些對象共享。
    ③ 內(nèi)部狀態(tài)獨立于具體的場景,通常不會改變。
    ④ 外部狀態(tài)取決于具體的場景,并根據(jù)場景而變化,外部狀態(tài)不能被共享。

2.14 策略模式

  1. 介紹
    定義一系列的算法,把不同策略封裝起來,并且使他們可以相互替換。
    注釋:策略模式可以替代if...else...語句。
  2. 代碼演示
//不使用策略模式
class User {
  constructor(type) {
    this.type = type
  }

  buy() {
    if(this.type === 'oridinary') {
      console.log('普通用戶購買')
    } else if(this.type === 'member') {
      console.log('會員用戶購買')
    } else if('this.type' === 'vip') {
      console.log('vip 用戶購買')
    }
  }
}

let u1 = new User('member') 
u1.buy() //會員用戶購買

//使用策略模式
class OridinaryUser {
  buy() {
    console.log('普通用戶購買')
  }
}
class MemberUser {
  buy() {
    console.log('會員用戶購買')
  }
}

class VipUser {
  buy() {
    console.log('vip 用戶購買')
  }
}

class UserManager {
  constructor() {
    this.user = null
  }

  setUser(user) {
    this.user= user
  }

  userBuy() {
    this.user.buy()
  }
}

const m = new UserManager()
const u = new OridinaryUser()
m.setUser(u)
m.userBuy() //普通用戶購買

注釋:策略模式的實現(xiàn)至少由兩部分組成。第一個部分是一組策略類(OridinaryUser、MemberUser、VipUser),策略類封裝了具體的算法,并負(fù)責(zé)具體的計算過程。第二個部分是環(huán)境類(UserManager),環(huán)境類接受客戶的請求,隨后把請求委托給某一個策略類。

  1. JavaScript中的策略模式
    策略對象從各個策略類中創(chuàng)建而來,環(huán)境對象從環(huán)境類中創(chuàng)建而來,這是模擬一些傳統(tǒng)面向?qū)ο笳Z言的實現(xiàn)。在JavaScript中,函數(shù)也是對象,所以更簡單和直接的做法是把策略對象和環(huán)境對象直接定義為函數(shù)。
const strategies = {
  oridinary() {
    console.log('普通用戶購買')
  },
  member() {
    console.log('會員用戶購買')
  },
  vip() {
    console.log('vip 用戶購買')
  }
}

const userBuy = user => strategies[user]()
userBuy('member') //會員用戶購買
  1. 項目應(yīng)用場景總結(jié)
    ① 參數(shù)面板兩種動畫。
    ② 目錄跳轉(zhuǎn)cpt/frm。

2.15 模板方法模式

  1. 介紹
    在抽象父類中封裝了子類的算法框架,包括實現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序。子類通過繼承這個抽象類,也繼承了整個算法結(jié)構(gòu),并且可以選擇重寫父類的方法。
  2. 代碼演示
class Beverage {
  init() {
    this.boilWater()
    this.brew()
    this.pourInCup()
    this.addCondiments()
  }

  boilWater() {
    console.log('把水煮沸')
  }

  brew(){} //沸水沖泡飲料
  pourInCup(){} //飲料倒進(jìn)杯子
  addCondiments(){} //加調(diào)料
}

class Coffee extends Beverage {
  brew(){
    console.log('用沸水沖泡咖啡')
  } 
  pourInCup(){
    console.log('把咖啡倒進(jìn)杯子')
  } 
  addCondiments(){
    console.log('加糖和牛奶')
  } 
}

class Tea extends Beverage {
  brew(){
    console.log('用沸水浸泡茶葉')
  } 
  pourInCup(){
    console.log('把茶倒進(jìn)杯子')
  } 
  addCondiments(){
    console.log('加檸檬')
  } 
}

const coffee = new Coffee()
coffee.init()

const tea = new Tea()
tea.init()
  1. 模式對比
    策略模式和模板方法是一對競爭者。在大多數(shù)情況下,他們可以相互替換使用。模板方法模式基于繼承的思想,而策略模式則偏重于組合和委托。

2.16 職責(zé)鏈模式

  1. 介紹
    使多個對象都有機(jī)會處理請求,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。
  2. 代碼演示
    ① 流程審批(職責(zé)鏈每個節(jié)點都處理)
class Action {
  constructor(name) {
    this.name = name
    this.nextAction = null
  }

  setNextAction(action) {
    this.nextAction = action
  }

  handle() {
    console.log(`${this.name} 審批`)
    this.nextAction && this.nextAction.handle()
  }
}

const action1 = new Action('組長')
const action2 = new Action('經(jīng)理')
const action3 = new Action('總監(jiān)')

action1.setNextAction(action2)
action2.setNextAction(action3)

action1.handle()

② 購買商品(職責(zé)鏈只有一個節(jié)點處理)

const order500 = (orderType, pay, stock) => {
  if(orderType === 1 && pay === true){
    console.log('500元定金預(yù)購,得到100優(yōu)惠券')
    return true
  }
  return false
}

const order200 = (orderType, pay, stock) => {
  if(orderType === 2 && pay === true){
    console.log('200元定金預(yù)購,得到50優(yōu)惠券')
    return true
  }
  return false
}

const orderNormal = (orderType, pay, stock) => {
  if(stock > 0){
    console.log('普通購買,無優(yōu)惠券')
  } else {
    console.log('手機(jī)內(nèi)存不足')
  }
  return true
}

class Chain {
  constructor(fn) {
    this.fn = fn
    this.successor = null
  }

  setNextSuccessor(successor){
    this.successor = successor
  }

  passRequest() {
    const res = this.fn.apply(this, arguments)
    return res ? res : this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
}

const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const chainOrderNormal = new Chain(orderNormal)

chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

chainOrder500.passRequest(1, true, 500) //500元定金預(yù)購,得到100優(yōu)惠券
chainOrder500.passRequest(2, true, 500) //200元定金預(yù)購,得到50優(yōu)惠券
chainOrder500.passRequest(3, false, 500) //普通購買,無優(yōu)惠券
chainOrder500.passRequest(3, false, 0) //手機(jī)內(nèi)存不足
  1. 使用場景
    ① 作用域鏈
    ② 原型鏈
    ③ 事件冒泡
    jQuery的鏈?zhǔn)讲僮?br> ⑤ Promise.then的鏈?zhǔn)讲僮?/li>
  2. 項目應(yīng)用場景總結(jié)
    createZoomSelectComponent方法中使用職責(zé)鏈模式替換if...else...。

2.17 命令模式

  1. 介紹
    執(zhí)行命令時,發(fā)布者和執(zhí)行者分開,中間加入命令對象,作為中轉(zhuǎn)站。
  2. 應(yīng)用場景
    有時候需要向某些對象發(fā)送請求,但是并不知道請求的接收者是誰,也不知道被請求的操作是什么。此時希望用一種松耦合的方式來設(shè)計程序,使得請求發(fā)送者和請求接受者能夠消除彼此之間的耦合關(guān)系。
  3. 代碼演示
//接受者
class Receiver{
  exec() {
    console.log('執(zhí)行')
  }
}

//命令者
class  Command {
  constructor(receiver) {
    this.receiver = receiver
  }

  cmd() {
    console.log('執(zhí)行命令')
    this.receiver.exec()
  }
}

//觸發(fā)者
class Invoker {
  constructor(command) {
    this.command = command
  }

  invoke() {
    console.log('開始')
    this.command.cmd()
  }
}

//士兵
const soldier = new Receiver()
//小號手
const trumpeter = new Command(soldier)
//將軍
const general = new Invoker(trumpeter)

general.invoke()
  1. JavaScript中的命令模式
    命令模式的由來,其實是回調(diào)(callback)函數(shù)的一個面向?qū)ο蟮奶娲贰?code>JavaScript將函數(shù)作為一等對象,與策略模式一樣,命令模式早已融入到了JavaScript語言中。運算塊可以封裝在普通函數(shù)中。函數(shù)作為一等對象,本身就可以被四處傳遞。
const bindClick = (button, func) => {
  button.onclick = func
}

const MenuBar = {
  refresh() {
    console.log('刷新菜單界面')
  }
}

const SubMenu = {
  add() {
    console.log('增加子菜單')
  },
  del() {
    console.log('刪除子菜單')
  }
}

bindClick(button1, MenuBar.refresh)
bindClick(button2, SubMenu.add)
bindClick(button3, SubMenu.del)

2.18 備忘錄模式

  1. 介紹
    隨時記錄一個對象的狀態(tài)變化。隨時可以恢復(fù)之前的某個狀態(tài)(如撤銷功能)。
  2. 代碼演示
//備忘類
class Memento {
  constructor(content) {
    this.content = content
  }

  getContent() {
    return this.content
  }
}

//備忘列表
class CareTaker {
  constructor() {
    this.list = []
  }

  add(memento) {
    this.list.push(memento)
  }

  get(index) {
    return this.list[index]
  }
}

//編輯器
class Editor {
  constructot() {
    this.content =  null
  }

  setContent(content) {
    this.content = content
  }

  getContent() {
    return this.content
  }

  saveContentToMemento() {
    return new Memento(this.content)
  }

  setContentFromMemento(mement) {
    return this.content = mement.getContent()
  }
 }

 //測試代碼
const editor = new Editor()
const careTaker = new CareTaker()

editor.setContent('1')
editor.setContent('2')
careTaker.add(editor.saveContentToMemento())
editor.setContent('3')
careTaker.add(editor.saveContentToMemento())
editor.setContent('4')
console.log(editor.getContent()) // 4
editor.setContentFromMemento(careTaker.get(1)) //撤銷
console.log(editor.getContent()) // 3 
editor.setContentFromMemento(careTaker.get(0)) //撤銷
console.log(editor.getContent()) // 2

2.19 中介者模式

  1. 介紹
    解除對象與對象之間的緊耦合關(guān)系。增加一個中介者對象后,所有的相關(guān)對象都通過中介者對象來通訊,而不是互相引用。當(dāng)一個對象發(fā)生改變時,只需要通知中介者對象即可。
  2. 中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系


    image.png
  3. 現(xiàn)實中的中介者
    ① 機(jī)場指揮塔
    ② 博彩公司
  4. 代碼演示
class A {
  constructor() {
    this.number = 0
  }

  setNumber(num, m) {
    this.number = num
    if(m) {
      m.setB()
    }
  }
}

class B {
  constructor() {
    this.number = 0
  }

  setNumber(num, m) {
    this.number = num
    if(m) {
      m.setA()
    }
  }
}

//中介者
class Mediator {
  constructor(a, b) {
    this.a = a
    this.b = b
  }

  setB() {
    let num = this.a.number
    this.b.setNumber(num / 10)
  }

  setA() {
    let num = this.b.number
    this.a.setNumber(num + 5)
  }
}

const a = new A()
const b = new B()
const m = new Mediator(a, b)
a.setNumber(100, m)
console.log(a.number, b.number)

b.setNumber(100, m)
console.log(a.number, b.number)

2.20 訪問者模式

  1. 介紹
    將數(shù)據(jù)操作與數(shù)據(jù)結(jié)構(gòu)進(jìn)行分離。

2.21 解釋器模式

  1. 介紹
    描述語言語法如何定義,如何解釋和編譯。

3. 綜合案例

3.1 模擬購物車

  1. 案例分析
    (1) 使用jQuery做一個模擬購物車示例。
    (2) 顯示購物列表、加入購物車、從購物車刪除
  2. 涉及到的設(shè)計模式
    (1) 創(chuàng)建型
    工廠模式、單例模式
    (2) 結(jié)構(gòu)型
    裝飾器模式、代理模式
    (3) 行為型
    觀察者模式、狀態(tài)模式、模板方法模式
  3. UML類圖
    image.png
  4. 代碼演示
    imooc-design-mode

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容