JavaScript面向?qū)ο笈c設(shè)計模式

1. 面向?qū)ο?/h2>

1.1 封裝

封裝的目的在于將信息隱藏鸭叙。廣義的封裝不僅包括封裝數(shù)據(jù)封裝實現(xiàn),還包括封裝類型封裝變化生百。

1.1.1 封裝數(shù)據(jù)

  1. Java中递雀,封裝數(shù)據(jù)是由語法解析來實現(xiàn)的,提供了public蚀浆、protected缀程、private等關(guān)鍵字來提供不同的訪問權(quán)限。
  2. JavaScript中市俊,只能依賴變量作用域來模擬實現(xiàn)封裝性杨凑。
var myObj = (function() {
  var _name = 'sven'
  return {
    getName: function() {
      return _name
    }
  }
})()

console.log(myObj.getName()) // sven
console.log(myObj._name) // undefined

1.1.2 封裝實現(xiàn)

  1. 封裝實現(xiàn)細節(jié)使對象內(nèi)部的變化對其他對象而言是透明的,即不可見的摆昧。
  2. 封裝實現(xiàn)細節(jié)使對象之間的耦合變松散撩满,對象之間只通過暴露的API接口來通訊。當我們修改一個對象時绅你,可以隨意修改它的內(nèi)部實現(xiàn)伺帘。
  3. 封裝實現(xiàn)細節(jié)的例子有很多。例如:迭代器each函數(shù)忌锯。

1.1.3 封裝類型

  1. 對于靜態(tài)類型語言伪嫁,封裝類型是通過抽象類和接口來進行的。將對象的真正類型隱藏在抽象類或者接口之后偶垮。
  2. JavaScript是一門類型模糊語言张咳。在封裝類型方面,JavaScript沒有能力似舵,也沒有必要做得更多脚猾。
  3. 對于JavaScript設(shè)計模式實現(xiàn)來說,不區(qū)分類型是一種失色砚哗,也可以說是一種解脫龙助。

1.1.4 封裝變化

  1. 考慮你的設(shè)計中哪些地方可能變化,找到并封裝蛛芥,這是許多設(shè)計模式的主題泌参。
  2. 設(shè)計模式被劃分為創(chuàng)建型模式脆淹、結(jié)構(gòu)型模式以及行為型模式。其中沽一,創(chuàng)建型模式的目的就是封裝創(chuàng)建對象的變化,結(jié)構(gòu)型模式封裝的是對象之間的組合關(guān)系漓糙,行為型模式封裝的是對象的行為變化铣缠。
  3. 通過封裝變化的方式,把系統(tǒng)中穩(wěn)定不變的部分和容易變化的部分隔離開來昆禽,在系統(tǒng)的演變過程中蝗蛙,我們只需要替換那些容易變化的部分,如果這些部分是已經(jīng)封裝好的醉鳖,替換起來也相對容易捡硅。這可以最大限度的保證程序的穩(wěn)定性和可擴展性。

1.2 繼承

  1. JavaScript選擇了基于原型的面向?qū)ο笙到y(tǒng)盗棵。在原型編程的思想中壮韭,類并不是必須的,對象未必從類中創(chuàng)建而來纹因,一個對象可以通過克隆另一個對象而得到喷屋。
  2. 雖然JavaScript的對象最初都是由Object.prototype對象克隆而來的,但對象構(gòu)造器的原型可以動態(tài)指向其它對象瞭恰。這樣一來屯曹,當對象a需要借用對象b的能力時,可以有選擇性地把對象a的構(gòu)造器的原型指向?qū)ο?code>b惊畏,從來達到繼承的效果恶耽。
  3. 原型繼承
var obj = {name: 'sven'}
var A = function() {}

A.prototype = obj

var a = new A()
console.log(a.name) //sven

name屬性查找:a → a.__proto__→ obj

  1. 原型繼承鏈
var obj = {name: 'sven'}

var A = function() {}
A.prototype = obj

var B = function() {}
B.prototype = new A()

var b = new B()
console.log(b.name) //sven

name屬性查找鏈:b → b.__proto__ → new A() → A.prototype → obj

1.3 多態(tài)

  1. 多態(tài)將“做什么”和“誰去做”分離開來。實現(xiàn)多態(tài)的關(guān)鍵在于消除類型之間的耦合關(guān)系颜启。
  2. Java中偷俭,可以通過向上轉(zhuǎn)型來實現(xiàn)多態(tài)。由于JavaScript的變量類型在運行期是可變的农曲,所以JavaScript對象的多態(tài)性是與生俱來的咒林。
  3. 多態(tài)的最根本好處在于,你不必再向?qū)ο笤儐枴澳闶鞘裁搭愋汀倍蟾鶕?jù)得到的答案調(diào)用對象的某個行為——你只管調(diào)用該行為就是了忍捡,其他的一切多態(tài)機制都會為你安排妥當擦秽。
  4. 多態(tài)將過程化的條件分支語句轉(zhuǎn)化為對象的多態(tài)性,從而消除這些條件分支語句暮的。
  5. 代碼演示
class GoogleMap {
  show() {
    console.log('開始渲染谷歌地圖')
  }
}

class BaiduMap {
  show() {
    console.log('開始渲染百度地圖')
  }
}

const renderMap = map => {
  if(map.show instanceof Function) {
    map.show()
  }
}

renderMap(new GoogleMap())
renderMap(new BaiduMap())

1.4 UML類圖

  1. 類圖


    類圖
  2. 類與類之間的關(guān)系
    (1) 泛化表示繼承笙以,用空心箭頭表示。
    (2) 關(guān)聯(lián)表示引用冻辩,用實心箭頭表示猖腕。


    類與類關(guān)系圖

1.5 示例

  1. 使用class簡單實現(xiàn)jQuery中的$選擇器
class jQuery {
  constructor(selector) {
    const slice = Array.prototype.slice
    const dom = slice.call(document.querySelectorAll(selector))
    this.selector = selector || ''
    const len = dom ? dom.length : 0;
    this.length = len
    for(let i = 0; i < len; i++) {
      this[i] = dom[i]
    }
  }
  append(node) {}
  addClass(name) {}
  html(data) {}
}

window.$ = selector => new jQuery(selector)
  1. 打車時可以打?qū)\嚮蚩燔嚥鹌怼H魏诬嚩加熊嚺铺柡兔Q√雀校快車每公里1元放坏,專車每公里2元。行程開始時老玛,顯示車輛信息淤年。行程結(jié)束時,顯示打車金額蜡豹。行程距離為5公里麸粮。


    UML類圖
//父類 - 車
class Car {
  constructor(name, number) {
    this.name = name
    this.number = number
  }
}

//子類 - 快車
class KuaiChe extends Car {
  constructor(name, number) {
    super(name, number)
    this.price = 1
  }
}

//子類 - 專車
class ZhuanChe extends Car {
  constructor(name, number) {
    super(name, number)
    this.price = 2
  }
}

//行程
class Trip {
  constructor(car, distance) {
    this.car = car
    this.distance = distance
  }

  start() {
    console.log(`行程開始 車名為${this.car.name},車牌號為${this.car.number}`)
  }

  end() {
    console.log(`行程結(jié)束 車費為${this.distance * this.car.price}`)
  }
}

const zhuanChe = new ZhuanChe('專車', '299567')
const trip = new Trip(zhuanChe, 5)
trip.start()
trip.end()

注意:將行程抽象為一個類镜廉,而不是車的一個屬性弄诲。

  1. 某停車廠分3層,每層100個車位娇唯。每個車位都能夠檢測到車輛的駛?cè)牒碗x開齐遵。車輛進入前,顯示每層空余車位數(shù)量视乐。車輛進入時洛搀,攝像頭可識別車牌號和時間。車輛出來時佑淀,出口顯示器顯示車牌號和停車時間留美。
    類: 停車場、層伸刃、車位谎砾、車輛、攝像頭捧颅、顯示器景图。


    UML類圖
//車輛
class Car {
  constructor(number) {
    this.number = number
  }
}

//停車位
class Stall {
  constructor() {
    this.empty = true
  }

  in() {
    this.empty = false
  }

  out() {
    this.empty = true
  }
}

//停車層
class Floor {
  constructor(index, stalls) {
    this.index = index
    this.stalls = stalls || []
  }

  emptyNum() {
    return this.stalls.filter(stall => stall.empty).length
  }
}

//出口顯示屏
class Screen {
  show(car, inTime) {
    console.log(`車牌號為${car.number},停留時間為${Date.now() - inTime}`)
  }
} 

//入口攝像頭
class Camera {
  shoot(car) {
    return {
      number: car.number,
      inTime: Date.now()
    }
  }
}

//停車場
class Park {
  constructor(floors, camera, screen) {
    this.camera = camera
    this.screen = screen
    this.floors = floors || []
    this.carList = {};
  }

  emptyNum() {
    let num = 0
    this.floors.forEach(floor => {
      const emptyNum = floor.emptyNum()
      num += emptyNum
    })
    return num;
  }

  showMsg() {
    let info = ''
    for(let i = 1; i < this.floors.length; i++) {
      const floor = this.floors[i]
      info += `第${floor.index}層還有${floor.emptyNum()}個空位`
    }
    console.log(info)
  }

  in(car) {
    if(this.emptyNum() > 0) {
      const info = this.camera.shoot(car)
      for(let i = 1; i < this.floors.length; i ++) {
        const floor = this.floors[i]
        const allNum = floor.stalls.length
        const emptyNum = floor.emptyNum()
        if(emptyNum > 0) {
          let index = 1; 
          while(!floor.stalls[index].empty) {
            index++
          }
          const stall = floor.stalls[index]
          stall.in()
          //保存停車位信息
          info.stall = stall
          break
        }
      } 
      this.carList[car.number] = info
    } else {
      console.log('停車場已滿')
    }
  }

  out(car) {
    const info = this.carList[car.number]
    info.stall.out()
    this.screen.show(car, info.inTime)
    delete this.carList[car.number]
  }
}

//測試代碼
const floors = []
for(let i = 1; i <= 3; i ++) {
  const stalls = []
  for(let j = 1; j <= 100; j++) {
    stalls[j] = new Stall()
  }
  floors[i] = new Floor(i, stalls)
}

const camera = new Camera()
const screen = new Screen()
const park = new Park(floors, camera, screen)
const car1 = new Car('100')
const car2 = new Car('200')
const car3 = new Car('300')
park.in(car1)
park.showMsg()
park.in(car2)
park.showMsg()
park.out(car1)
park.showMsg()
park.in(car3)
park.showMsg()
park.out(car2)
park.showMsg()
park.out(car3)
park.showMsg()

注意:① 引用指的是一個類持有另一個類,而不是一個類的方法以另一個類為參數(shù)碉哑。 ② 類與類之間應(yīng)該盡量減少耦合挚币,能夠通過將另一個類作為參數(shù)實現(xiàn),就不要持有另一個類扣典。

2. 設(shè)計模式

  1. 簡介
    在面向?qū)ο筌浖O(shè)計過程中針對特定問題的簡潔而優(yōu)雅的解決方案妆毕。即設(shè)計模式是在某種場合下對某個問題的一種解決方案。
  2. 分類
    (1) 創(chuàng)建型
    工廠模式贮尖、單例模式笛粘、原型模式。
    (2) 結(jié)構(gòu)型
    適配器模式、裝飾器模式薪前、代理模式润努、外觀模式、橋接模式示括、組合模式铺浇、享元模式。
    (3) 行為型
    策略模式垛膝、模板方法模式随抠、觀察者模式、迭代器模式繁涂、職責鏈模式、命令模式二驰、備忘錄模式扔罪、狀態(tài)模式、訪問者模式桶雀、中介者模式矿酵、解釋器模式。

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)建對象與管理單例的職責被分布在兩個不同的方法中。

  1. 使用場景

(1) jQuery中只有一個$

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

(2) vuexredux中的store

2.3 適配器模式

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

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

  request() {
    let info = this.adaptee.specifiRequest()
    return `${info} - 轉(zhuǎ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)地給某個對象添加額外的職責,而不會影響從這個類中派生的其他對象。
  2. 模式分析
    在傳統(tǒng)的面向?qū)ο笳Z言中示姿,給對象添加功能常常使用繼承的方式甜橱。繼承的方式并不靈活,會導致超類和子類存在強耦合性栈戳,并且在完成一些功能復(fù)用的同時岂傲,有可能創(chuàng)建出大量的子類。
    裝飾器模式能夠在不改變對象自身的基礎(chǔ)上子檀,在程序運行期間給對象動態(tài)地添加職責镊掖。與繼承相比,裝飾者是一種更輕便靈活的做法褂痰,這是一種“即用即付”的方式亩进。
  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ā)射導彈')
  }
}

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ā)射導彈 發(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 } 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>'

2.5 代理模式

  1. 介紹
    使用者無權(quán)訪問目標對象归薛,通過代理做授權(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
  debugger;
  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)紀人
let agent = new Proxy(star, {
  get: function (target, key, receiver) {
    if(key === 'phone') {
      //返回經(jīng)紀人自己的電話
      return '13922225555'
    }
    if(key === 'price') {
      //明星不報價,經(jīng)紀人報價
      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) 代理模式與裝飾器模式
    裝飾器模式:擴展功能,原有功能不變且可直接使用颤绕。
    代理模式:使用原有功能幸海,但是經(jīng)過限制和約束。

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)系汗洒,當一個對象的狀態(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瞻坝、Map蛛壳、SetStringTypedArray等衙荐。需要有一個統(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)

2.9 狀態(tài)模式

  1. 介紹
    將狀態(tài)封裝成獨立的類溜族,并將請求委托給當前的狀態(tài)對象讹俊,當對象的內(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('強光')
    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() //強光
light.press() //關(guān)燈

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

  1. 使用場景
    (1) 有限狀態(tài)機
    有限個狀態(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 - 當前狀態(tài)機實例; data - fsm.resolve(xxx) 傳遞的參數(shù)
      data.successList.forEach(fn => fn())
    },
    onReject(state, data){
      //state - 當前狀態(tài)機實例; 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)部降狠,客戶并不需要了解這些細節(jié)。

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.prototype骑冗,Object.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)點在于可以一致地對待組合對象和基本對象袒哥∷跎福客戶不需要知道當前處理的是宏任務(wù)還是普通任務(wù)。
  2. 特點
    ① 使用樹形方式創(chuàng)建對象的結(jié)構(gòu)堡称。
    ② 把相同操作應(yīng)用在組合對象和單個對象上瞎抛。
  3. 使用場景
    虛擬DOM中的vnode是這種形式。
  4. 組合模式注意事項
    ① 組合對象和葉對象是聚合關(guān)系而非父子關(guān)系却紧。
    ② 組合對象和葉對象擁有相同的接口桐臊。
    ③ 對組合對象和葉對象的操作必須具有一致性。

2.13 享元模式

  1. 介紹
    享元模式是一種用于性能優(yōu)化的模式晓殊,享元模式的核心是運用共享技術(shù)來有效支持大量細粒度的對象断凶。
  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)至少由兩部分組成柱彻。第一個部分是一組策略類(OridinaryUserMemberUser餐胀、VipUser)哟楷,策略類封裝了具體的算法,并負責具體的計算過程否灾。第二個部分是環(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') //會員用戶購買

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(){} //飲料倒進杯子
  addCondiments(){} //加調(diào)料
}

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

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

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

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

2.16 職責鏈模式

  1. 介紹
    使多個對象都有機會處理請求论衍,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系瑞佩,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求坯台,直到有一個對象處理它為止炬丸。
  2. 代碼演示
    ① 流程審批(職責鏈每個節(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()

② 購買商品(職責鏈只有一個節(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('手機內(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) //手機內(nèi)存不足
  1. 使用場景
    ① 作用域鏈
    ② 原型鏈
    ③ 事件冒泡
    jQuery的鏈式操作
    Promise.then的鏈式操作

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)對象都通過中介者對象來通訊饼记,而不是互相引用香伴。當一個對象發(fā)生改變時,只需要通知中介者對象即可具则。
  2. 中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系


    image.png
  3. 現(xiàn)實中的中介者
    ① 機場指揮塔
    ② 博彩公司
  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)進行分離即纲。

2.21 解釋器模式

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

3. 設(shè)計原則和編程技巧

3.1 設(shè)計原則概述

  1. UNIX/LINUX設(shè)計哲學》設(shè)計準則
    ① 小既是美低斋。
    ② 每個程序只做一件事情。
    ③ 快速建立原型匪凡。
    ④ 舍棄高效率而取可移植性膊畴。
    ⑤ 避免強制性的圖形化界面交互。
    ⑥ 讓每個程序都成為過濾器病游。
    ⑦ 尋求90%的解決方案唇跨。
    注釋:花20%的成本解決80的需求。
  2. 五大設(shè)計原則(SOLID)
    S - 單一職責原則
    O - 開放封閉原則
    L - 李氏置換原則
    I - 接口獨立原則
    D - 依賴倒置原則
  3. 單一職責原則
    一個程序只做好一件事情衬衬。
  4. 開放封閉原則
    對擴展開放买猖,對修改封閉。
    增加需求時滋尉,擴展新代碼玉控,而非修改已有代碼。
  5. 李氏置換原則
    子類能覆蓋父類狮惜。
    父類能出現(xiàn)的地方子類就能出現(xiàn)奸远。
  6. 接口獨立原則
    保持接口的獨立既棺,避免出現(xiàn)“胖接口”。
  7. 依賴倒置原則
    面向接口編程懒叛,依賴于抽象而不依賴于具體丸冕。

3.2 單一職責原則

  1. 簡介
    就一個類、對象以及方法而言薛窥,應(yīng)該僅有一個引起它變化的原因胖烛。
    注釋:單一職責原則定義為“引起變化的原因”。如果我們有兩個動機去改寫一個方法诅迷,那么這個方法就具有兩個職責佩番。
  2. 原則
    一個對象(方法)只做一件事情。
  3. 設(shè)計模式驗證
    ① 代理模式
    圖片預(yù)加載代理模式中罢杉,代理對象負責預(yù)加載職責趟畏,本體對象負責圖片加載職責。
    ② 迭代器模式
    迭代器模式提供遍歷訪問聚合對象的職責滩租。
    ③ 單例模式
    將創(chuàng)建對象與管理單例分別封裝在兩個方法中赋秀,兩個方法相互獨立互不影響。
    ④ 裝飾者模式
    讓類或者對象一開始只具有一些基礎(chǔ)的職責律想,更多的職責在代碼運行時被動態(tài)裝飾到對象上面猎莲。裝飾者模式可以為對象動態(tài)增加職責,從另一個角度來看技即, 這也是分離職責的一種方式著洼。
  4. 應(yīng)用場景
    ① 如果有兩個職責總是同時變化,那就不必分離他們而叼。即使兩個職責已經(jīng)被耦合在一起身笤,但它們還沒有發(fā)生改變的征兆,那么也許沒有必要主動分離它們葵陵。
    ② 在方便性與穩(wěn)定性之間要有一些取舍液荸。具體是選擇方便性還是穩(wěn)定性,并沒有標準答案埃难,而是要取決于具體的應(yīng)用環(huán)境。例如:jQueryattr 是個非常龐大的方法涤久,既負責賦值涡尘,又負責取值,這對于jQuery的維護者來說响迂,會帶來一些困難考抄,但對于jQuery的用戶來說莱预,卻簡化了用戶的使用苍日。
  5. 優(yōu)缺點
    ① 優(yōu)點
    按照職責把對象分解成更小的粒度,降低了單個類或者對象的復(fù)雜度,有助于代碼的復(fù)用铃诬,也有利于進行單元測試。
    ② 缺點
    增加了編寫代碼的復(fù)雜度擂达,也增大了這些對象之間相互聯(lián)系的難度蹋肮。

3.3 最少知識原則

  1. 簡介
    最少知識原則(LKP)指一個軟件實體應(yīng)當盡可能少地與其他實體發(fā)生相互作用。這里的軟件實體不僅包括對象丢早,還包括系統(tǒng)姨裸、類、模塊怨酝、函數(shù)傀缩、變量等。
  2. 減少對象之間的聯(lián)系
    單一職責原則指導我們把對象劃分成較小的粒度农猬,提高對象的可復(fù)用性赡艰。但越來越多的對象之間可能會產(chǎn)生錯綜復(fù)雜的聯(lián)系,如果修改了其中一個對象斤葱,很可能會影響到跟它相互引用的其他對象慷垮。
    最少知識原則要求我們盡量減少對象之間的交互。如果兩個對象之間不必彼此直接通信苦掘,那么這兩個對象就不要發(fā)生直接的相互聯(lián)系换帜。
  3. 設(shè)計模式驗證
    ① 中介者模式
    增加一個中介者對象,讓所有的相關(guān)對象都通 過中介者對象來通信鹤啡,而不是互相引用惯驼。當一個對象發(fā)生改變時,只需要通知中介者對象即可递瑰。
    ② 外觀模式
    外觀模式對客戶提供一個簡單易用的高層接口祟牲,高層接口會把客戶的請求轉(zhuǎn)發(fā)給子系統(tǒng)來完成具體的功能實現(xiàn)。

3.4 開放-封閉原則

  1. 簡介
    軟件實體(類抖部、模塊说贝、函數(shù))等應(yīng)該是可以擴展的,但是不可修改慎颗。
  2. 原則
    當需要改變一個程序的功能或者給這個程序增加新功能的時候乡恕,可以使用增加代碼的方式,但是不允許改動程序的源代碼俯萎。
  3. 實現(xiàn)方式
    通過封裝變化的方式傲宜,可以把系統(tǒng)中穩(wěn)定不變的部分和容易變化的部分隔離開來。
    (1) 利用對象多態(tài)性
    利用對象的多態(tài)性來消除條件分支語句夫啊。
    (2) 放置掛鉤
    在程序有可能發(fā)生變化的地方放置一個掛鉤函卒,掛鉤的返回結(jié)果決定了程序的下一步走向。
    (3) 回調(diào)函數(shù)
    把一部分易于變化的邏輯封裝在回調(diào)函數(shù)里撇眯,然后把回調(diào)函數(shù)當作參數(shù)傳入一個穩(wěn)定和封閉的函數(shù)中报嵌。
  4. 設(shè)計模式驗證
    ① 觀察者模式
    當有新的訂閱者出現(xiàn)時虱咧,發(fā)布者的代碼不需要進行任何修改;同樣當發(fā)布者需要改變時锚国,也不會影響到之前的訂閱者腕巡。
    ② 模板方法模式
    子類的方法種類和執(zhí)行順序都是不變的,所以我們把這部分邏輯抽出來放到父類的模板方法里面跷叉;而子類的方法具體怎么實現(xiàn)則是可變的逸雹,于是把這部分變化的邏輯封裝到子類中。通過增加新的子類云挟,便能給系統(tǒng)增加新的功能梆砸,并不需要改動抽象父類以及其他的子類。
    ③ 策略模式
    策略模式將各種算法都封裝成單獨的策略類园欣,這些策略類可以被交換使用帖世。策略和使用策略的客戶代碼可以分別獨立進行修改而互不影響。
    ④ 代理模式
    圖片預(yù)加載示例中沸枯,代理函數(shù)proxyMyImage負責圖片預(yù)加載日矫,myImage圖片加載函數(shù)不需要任何改動。
    ⑤ 職責鏈模式
    新增處理函數(shù)時绑榴,不需要改動原有的鏈條節(jié)點代碼哪轿,只需要在鏈條中增加一個新的節(jié)點。

3.5 代碼重構(gòu)

  1. 提煉函數(shù)
    如果在函數(shù)中有一段代碼可以被獨立出來翔怎,那我們最好把這些代碼放進另外一個獨立的函數(shù)中窃诉。
var getUserInfo = function () {
  ajax('http:// xxx.com/userInfo', function (data) {
    console.log('userId: ' + data.userId);
    console.log('userName: ' + data.userName);
    console.log('nickName: ' + data.nickName);
  })
}

//改成
var getUserInfo = function () {
  ajax('http:// xxx.com/userInfo', function (data) {
    printDetails(data);
  });
};
var printDetails = function (data) {
  console.log('userId: ' + data.userId);
  console.log('userName: ' + data.userName);
  console.log('nickName: ' + data.nickName);
};
  1. 合并重復(fù)的條件片段
    如果一個函數(shù)體內(nèi)有一些條件分支語句,而這些條件分支語句內(nèi)部散布了一些重復(fù)的代碼赤套,那么就有必要進行合并去重工作飘痛。
var paging = function (currPage) {
  if (currPage <= 0) {
    currPage = 0;
    jump(currPage); // 跳轉(zhuǎn) 
  } else if (currPage >= totalPage) {
    currPage = totalPage;
    jump(currPage);
  } else {
    jump(currPage);
  }
};

//改成
var paging = function (currPage) {
  if (currPage <= 0) {
    currPage = 0;
  } else if (currPage >= totalPage) {
    currPage = totalPage;
  }
  jump(currPage); // 把 jump 函數(shù)獨立出來 
};
  1. 把條件分支語句提煉成函數(shù)
    在程序設(shè)計中,復(fù)雜的條件分支語句是導致程序難以閱讀和理解的重要原因容握,而且容易導致一個龐大的函數(shù)宣脉。
var getPrice = function (price) {
  var date = new Date();
  if (date.getMonth() >= 6 && date.getMonth() <= 9) {
    return price * 0.8;
  }
  return price;
};

//改成
var isSummer = function () {
  var date = new Date();
  return date.getMonth() >= 6 && date.getMonth() <= 9;
};
var getPrice = function (price) {
  if (isSummer()) { // 夏天
    return price * 0.8;
  }
  return price;
};
  1. 合理使用循環(huán)
    在函數(shù)體內(nèi),如果有些代碼實際上負責的是一些重復(fù)性的工作剔氏,那么合理利用循環(huán)不僅可以 完成同樣的功能塑猖,還可以使代碼量更少。
var createXHR = function () {
  var xhr;
  try {
    xhr = new ActiveXObject('MSXML2.XMLHttp.6.0');
  } catch (e) {
    try {
      xhr = new ActiveXObject('MSXML2.XMLHttp.3.0');
    } catch (e) {
      xhr = new ActiveXObject('MSXML2.XMLHttp');
    }
  }
  return xhr;
};
var xhr = createXHR();

//改成
var createXHR = function () {
  var versions = ['MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
  for (var i = 0, version; version = versions[i++];) {
    try {
      return new ActiveXObject(version);
    } catch (e) {
    }
  }
};
var xhr = createXHR();
  1. 提前讓函數(shù)退出代替嵌套條件分支
    嵌套的條件分支語句絕對是代碼維護者的噩夢谈跛。嵌套的條件分支往往是由一些深信“每個函數(shù)只能有一個出口的”程序員寫出的羊苟。但實際上,如果對函數(shù)的剩余部分不感興趣币旧,那就應(yīng)該立即退出践险。
var del = function (obj) {
  var ret;
  if (!obj.isReadOnly) { // 不為只讀的才能被刪除
    if (obj.isFolder) { // 如果是文件夾
      ret = deleteFolder(obj);
    } else if (obj.isFile) {
      ret = deleteFile(obj);
    }
  }
  return ret;
};

var del = function (obj) {
  if (obj.isReadOnly) {
    return;
  }
  if (obj.isFolder) {
    return deleteFolder(obj);
  }
  if (obj.isFile) {
    return deleteFile(obj);
  }
};
  1. 傳遞對象參數(shù)代替過長的參數(shù)列表
    有時候一個函數(shù)有可能接收多個參數(shù)猿妈,而參數(shù)的數(shù)量越多吹菱,函數(shù)就越難理解和使用巍虫。在使用的時候,還要小心翼翼鳍刷,以免少傳了某個參數(shù)或者把兩個參數(shù)搞反了位置占遥。
    這時我們可以把參數(shù)都放入一個對象內(nèi),不用再關(guān)心參數(shù)的數(shù)量和順序输瓜,只要保證參數(shù)對應(yīng)的 key 值不變就可以了瓦胎。
  2. 盡量減少參數(shù)數(shù)量
    在實際開發(fā)中,向函數(shù)傳遞參數(shù)不可避免尤揣,但我們應(yīng)該盡量減少函數(shù)接收的參數(shù)數(shù)量搔啊。
  3. 少用三目運算符
    如果條件分支邏輯簡單且清晰,這無礙我們使用三目運算符北戏;但如果條件分支邏輯非常復(fù)雜负芋,我們最好的選擇還是按部就班地編寫 ifelse嗜愈。
  4. 合理使用鏈式調(diào)用
    經(jīng)常使用jQuery的程序員相當習慣鏈式調(diào)用方法旧蛾,在JavaScript 中,可以很容易地實現(xiàn)方法的鏈式調(diào)用蠕嫁,即讓方法調(diào)用結(jié)束后返回對象自身锨天。
    使用鏈式調(diào)用的方式可以省下一些字符和中間變量,但調(diào)試時非常不方便剃毒。如果我們知道一條鏈中有錯誤出現(xiàn)病袄,必須得先把這條鏈拆開才能加上一些調(diào)試log或者增加斷點。
    如果該鏈條的結(jié)構(gòu)相對穩(wěn)定迟赃,后期不易發(fā)生修改陪拘,那么使用鏈式調(diào)用無可厚非。但如果該鏈 條很容易發(fā)生變化纤壁,導致調(diào)試和維護困難左刽,那么還是建議使用普通調(diào)用的形式。
  5. 分解大型類
    面向?qū)ο笤O(shè)計鼓勵將行為分布在合理數(shù)量的更小對象之中酌媒。
  6. return退出多重循環(huán)
    假設(shè)在函數(shù)體內(nèi)有一個兩重循環(huán)語句欠痴,我們需要在內(nèi)層循環(huán)中判斷,當達到某個臨界條件時退出外層的循環(huán)秒咨。我們大多數(shù)時候會引入一個控制標記變量或設(shè)置循環(huán)標記喇辽。
    這兩種做法無疑都讓人頭暈?zāi)垦#唵蔚淖龇ㄊ窃谛枰兄寡h(huán)的時候直接退出整個方法雨席。
    如果在循環(huán)之后還有一些將被執(zhí)行的代碼菩咨,我們可以把循環(huán)后面的代碼放到 return 后面,如果代碼比較多,就應(yīng)該把它們提煉成一個單獨的函數(shù)抽米。

4. 綜合案例

4.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

5. 項目應(yīng)用場景總結(jié)

  1. 單例模式
    mainboard為單例,是否可以不聲明為類亡容,直接字面量對象嗤疯。
    ② 利用裝飾器模式,給類添加管理單例的方法闺兢,與創(chuàng)建對象的方法分離身弊。
    ③ 現(xiàn)有通過this._groupsMap緩存的方式使用單例模式替換。
  2. 裝飾器模式
    app監(jiān)控功能
    titleWidgetModel裝飾類
  3. 代理模式
    BaseLayout/utils.js中的createZoomSelectComponent方法使用緩存代理列敲。
  4. 職責鏈模式
    BaseLayout/utils.js中的createZoomSelectComponent方法使用職責鏈模式和緩存代理阱佛。
  5. 組合模式
    ① 布局與組件的關(guān)系可以使用組合模式。先創(chuàng)建整個表單的樹結(jié)構(gòu)戴而,再逐層遍歷凑术,而不是生成單層布局-控件結(jié)構(gòu)。
  6. 策略模式
    ① 參數(shù)面板兩種動畫所意。
    ② 目錄跳轉(zhuǎn)cpt/frm淮逊。
  7. 狀態(tài)模式
    ① 可放大組件的放大狀態(tài)。
    ② 可選中組件的選中狀態(tài)扶踊。
    ③ 參數(shù)面板展示隱藏狀態(tài)泄鹏。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市秧耗,隨后出現(xiàn)的幾起案子备籽,更是在濱河造成了極大的恐慌,老刑警劉巖分井,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件车猬,死亡現(xiàn)場離奇詭異,居然都是意外死亡尺锚,警方通過查閱死者的電腦和手機珠闰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘫辩,“玉大人伏嗜,你說我怎么就攤上這事坛悉。” “怎么了承绸?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵吹散,是天一觀的道長。 經(jīng)常有香客問我八酒,道長,這世上最難降的妖魔是什么刃唐? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任羞迷,我火速辦了婚禮,結(jié)果婚禮上画饥,老公的妹妹穿的比我還像新娘衔瓮。我一直安慰自己,他們只是感情好抖甘,可當我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布热鞍。 她就那樣靜靜地躺著,像睡著了一般衔彻。 火紅的嫁衣襯著肌膚如雪薇宠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天艰额,我揣著相機與錄音澄港,去河邊找鬼。 笑死柄沮,一個胖子當著我的面吹牛回梧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播祖搓,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼狱意,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拯欧?” 一聲冷哼從身側(cè)響起详囤,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎镐作,沒想到半個月后纬纪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡滑肉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年包各,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靶庙。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡问畅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情护姆,我是刑警寧澤矾端,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站卵皂,受9級特大地震影響秩铆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灯变,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一殴玛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧添祸,春花似錦滚粟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至耙替,卻和暖如春亚侠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背俗扇。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工盖奈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狐援。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓钢坦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親啥酱。 傳聞我的和親對象是個殘疾皇子爹凹,可洞房花燭夜當晚...
    茶點故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容

  • javascript設(shè)計模式與開發(fā)實踐 設(shè)計模式 每個設(shè)計模式我們需要從三點問題入手: 定義 作用 用法與實現(xiàn) 單...
    穿牛仔褲的蚊子閱讀 4,070評論 0 13
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,113評論 1 32
  • 1. 設(shè)計模式概述 簡介在面向?qū)ο筌浖O(shè)計過程中針對特定問題的簡潔而優(yōu)雅的解決方案。即設(shè)計模式是在某種場合下對某個...
    nimw閱讀 567評論 0 0
  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品镶殷,去做同樣的事情禾酱,實現(xiàn)同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,779評論 2 17
  • 我們總是在白天 期待著黑夜的降臨 可是黑夜來了 我們卻開始害怕起來了 幸福總會偽裝成疼痛的煎熬 在某個深夜里敲門
    一堇閱讀 802評論 7 14