[面試專題]JS設計模式

逺方小鎭發表於2019-02-16

JS設計模式


釋出訂閱模式:

這種設計模式可以大大降低程式模組之間的耦合度,便於更加靈活的擴充套件和維護。

// 一個播放器類
class Player {

  constructor() {
    // 初始化觀察者列表
    this.watchers = {}

    // 模擬2秒後釋出一個`play`事件
    setTimeout(() => {
      this._publish(`play`, true)
    }, 2000)

    // 模擬4秒後釋出一個`pause`事件
    setTimeout(() => {
      this._publish(`pause`, true)
    }, 4000)
  }

  // 釋出事件
  _publish(event, data) {
    if (this.watchers[event] && this.watchers[event].length) {
      this.watchers[event].forEach(callback => callback.bind(this)(data))
    }
  }

  // 訂閱事件
  subscribe(event, callback) {
    this.watchers[event] = this.watchers[event] || []
    this.watchers[event].push(callback)
  }

  // 退訂事件
  unsubscribe(event = null, callback = null) {
    // 如果傳入指定事件函式,則僅退訂此事件函式
    if (callback&&event) {
      if (this.watchers[event] && this.watchers[event].length) {
        this.watchers[event].splice(this.watchers[event].findIndex(cb => Object.is(cb, callback)), 1)
      }

    // 如果僅傳入事件名稱,則退訂此事件對應的所有的事件函式
    } else if (event) {
      this.watchers[event] = []

    // 如果未傳入任何引數,則退訂所有事件
    } else {
      this.watchers = {}
    }
  }
}

// 例項化播放器
const player = new Player()
console.log(player)

// 播放事件回撥函式1
const onPlayerPlay1 = function(data) {
  console.log(`1: Player is play, the `this` context is current player`, this, data)
}

// 播放事件回撥函式2
const onPlayerPlay2 = data => {
  console.log(`2: Player is play`, data)
}

// 暫停事件回撥函式
const onPlayerPause = data => {
  console.log(`Player is pause`, data)
}

// 載入事件回撥函式
const onPlayerLoaded = data => {
  console.log(`Player is loaded`, data)
}

// 可訂閱多個不同事件
player.subscribe(`play`, onPlayerPlay1)
player.subscribe(`play`, onPlayerPlay2)
player.subscribe(`pause`, onPlayerPause)
player.subscribe(`loaded`, onPlayerLoaded)

// 可以退訂指定訂閱事件
player.unsubscribe(`play`, onPlayerPlay2)
// 退訂指定事件名稱下的所有訂閱事件
player.unsubscribe(`play`)
// 退訂所有訂閱事件
player.unsubscribe()

// 可以在外部手動發出事件(真實生產場景中,釋出特性一般為類內部私有方法)
player._publish(`loaded`, true)

中介者模式 Mediator Pattern:

觀察者模式通過維護一堆列表來管理物件間的多對多關係,中介者模式通過統一介面來維護一對多關係,且通訊者之間不需要知道彼此之間的關係,只需要約定好API即可。

// 汽車
class Bus {

  constructor() {

    // 初始化所有乘客
    this.passengers = {}
  }

  // 釋出廣播
  broadcast(passenger, message = passenger) {
    // 如果車上有乘客
    if (Object.keys(this.passengers).length) {

      // 如果是針對某個乘客發的,就單獨給他聽
      if (passenger.id && passenger.listen) {

        // 乘客他愛聽不聽
        if (this.passengers[passenger.id]) {
          this.passengers[passenger.id].listen(message)
        }

      // 不然就廣播給所有乘客
      } else {
        Object.keys(this.passengers).forEach(passenger => {
          if (this.passengers[passenger].listen) {
            this.passengers[passenger].listen(message)
          }
        })
      }
    }
  }

  // 乘客上車
  aboard(passenger) {
    this.passengers[passenger.id] = passenger
  }

  // 乘客下車
  debus(passenger) {
    this.passengers[passenger.id] = null
    delete this.passengers[passenger.id]
    console.log(`乘客${passenger.id}下車`)
  }

  // 開車
  start() {
    this.broadcast({ type: 1, content: `前方無障礙,開車!Over`})
  }

  // 停車
  end() {
    this.broadcast({ type: 2, content: `老司機翻車,停車!Over`})
  }
}

// 乘客
class Passenger {

  constructor(id) {
    this.id = id
  }

  // 聽廣播
  listen(message) {
    console.log(`乘客${this.id}收到訊息`, message)
    // 乘客發現停車了,於是自己下車
    if (Object.is(message.type, 2)) {
      this.debus()
    }
  }

  // 下車
  debus() {
    console.log(`我是乘客${this.id},我現在要下車`, bus)
    bus.debus(this)
  }
}

// 建立一輛汽車
const bus = new Bus()

// 建立兩個乘客
const passenger1 = new Passenger(1)
const passenger2 = new Passenger(2)

// 倆乘客分別上車
bus.aboard(passenger1)
bus.aboard(passenger2)

// 2秒後開車
setTimeout(bus.start.bind(bus), 2000)

// 3秒時司機發現2號乘客沒買票,2號乘客被驅逐下車
setTimeout(() => {
  bus.broadcast(passenger2, { type: 3, content: `同志你好,你沒買票,請下車!` })
  bus.debus(passenger2)
}, 3000)

// 4秒後到站停車
setTimeout(bus.end.bind(bus), 3600)

// 6秒後再開車,車上已經沒乘客了
setTimeout(bus.start.bind(bus), 6666)

代理模式 Proxy Pattern:

為其他物件提供一種代理以控制對這個物件的訪問。
代理模式使得代理物件控制具體物件的引用。代理幾乎可以是任何物件:檔案,資源,記憶體中的物件,或者是一些難以複製的東西。

ES6中的Proxy物件

const target = {}
const handler = {
    get(target, property) {
        if (property in target) {
            return target[property]
        } else {
            throw new ReferenceError("Property "" + property + "" does not exist.")
        }
    }
}
const p = new Proxy(target, {})
p.a = 3  // 被轉發到代理的操作
console.log(p.c) //

單例模式 Singleton Pattern:

保證一個類只有一個例項,並提供一個訪問它的全域性訪問點(呼叫一個類,任何時候返回的都是同一個例項)。

實現方法:使用一個變數來標誌當前是否已經為某個類建立過物件,如果建立了,則在下一次獲取該類的例項時,直接返回之前建立的物件,否則就建立一個物件。

// 類數例項:
class Singleton {
  constructor(name) {
    this.name = name
    this.instance = null   // 
  }
  getName() {
    alert(this.name)
  }
  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name)
    }
    return this.instance
  }
}
const ins = new Singleton(`hhhh`)
const instanceA = Singleton.getInstance(`seven1`)
const instanceB = Singleton.getInstance(`seven2`)
//閉包包裝例項:
const SingletonP = (function() {
  let instance
  return class Singleton {

    constructor(name) {
      if (instance) {
        return instance
      } else {
        this.init(name)
        instance = this
        return this
      }
    }

    init(name) {
      this.name = name
      console.log(`已初始化`)
    }
  }
})()

const instanceA = new SingletonP(`seven1`)
const instanceB = new SingletonP(`seven2`)
// ES5 iife
var SingletonTester = (function () {
    function Singleton(args) {
        var args = args || {};
        //設定name引數
        this.name = `SingletonTester`;
    }
    //例項容器
    var instance;
    return {
        name: `SingletonTester`,
        getInstance: function (args) {
            if (instance === undefined) {
                instance = new Singleton(args);
            }
            return instance;
        }
    };
})();

var singletonTest = SingletonTester.getInstance({ pointX: 5 });
console.log(singletonTest.pointX); // 輸出 5 
// 建構函式的屬性
function Universe() {
    if (typeof Universe.instance === `object`) {
        return Universe.instance;
    }
    this.start_time = 0;
    this.bang = "Big";
    Universe.instance = this;
}
// 測試
var uni = new Universe();
var uni2 = new Universe();
console.log(uni === uni2); // true
// 重寫建構函式
function Universe() {
    var instance = this;
    // 其它內容
    this.start_time = 0;
    this.bang = "Big";
    // 重寫建構函式
    Universe = function () {
        return instance;
    };
}
// 測試
var uni = new Universe();
var uni2 = new Universe();
uni.bang = "123";
console.log(uni === uni2); // true
console.log(uni2.bang); // 123

工廠模式 Factory Pattern:

工廠模式定義一個用於建立物件的介面,這個介面由子類決定例項化哪一個類。該模式使一個類的例項化延遲到了子類。而子類可以重寫介面方法以便建立的時候指定自己的物件型別。

簡單說:假如我們想在網頁面裡插入一些元素,而這些元素型別不固定,可能是圖片、連結、文字,根據工廠模式的定義,在工廠模式下,工廠函式只需接受我們要建立的元素的型別,其他的工廠函式幫我們處理。

// 文字工廠
class Text {
    constructor(text) {
        this.text = text
    }
    insert(where) {
        const txt = document.createTextNode(this.text)
        where.appendChild(txt)
    }
}

// 連結工廠
class Link {
    constructor(url) {
        this.url = url
    }
    insert(where) {
        const link = document.createElement(`a`)
        link.href = this.url
        link.appendChild(document.createTextNode(this.url))
        where.appendChild(link)
    }
}

// 圖片工廠
class Image {
    constructor(url) {
        this.url = url
    }
    insert(where) {
        const img = document.createElement(`img`)
        img.src = this.url
        where.appendChild(img)
    }
}

// DOM工廠
class DomFactory {

  constructor(type) {
    return new (this[type]())
  }

  // 各流水線
  link() { return Link }
  text() { return Text }
  image() { return Image }
}

// 建立工廠
const linkFactory = new DomFactory(`link`)
const textFactory = new DomFactory(`text`)

linkFactory.url = `https://surmon.me`
linkFactory.insert(document.body)

textFactory.text = `HI! I am surmon.`
textFactory.insert(document.body)

裝飾者模式 Decorative Pattern:

裝飾者(decorator)模式能夠在不改變物件自身的基礎上,在程式執行期間給對像動態的新增職責(方法或屬性)。與繼承相比,裝飾者是一種更輕便靈活的做法。

簡單說:可以動態的給某個物件新增額外的職責,而不會影響從這個類中派生的其它物件。

ES7裝飾器
function isAnimal(target) {
    target.isAnimal = true
    return target
}

// 裝飾器
@isAnimal
class Cat {
    // ...
}
console.log(Cat.isAnimal)    // true



作用於類屬性的裝飾器:

function readonly(target, name, descriptor) {
    discriptor.writable = false
    return discriptor
}

class Cat {
    @readonly
    say() {
        console.log("meow ~")
    }
}

var kitty = new Cat()
kitty.say = function() {
    console.log("woof !")
}
kitty.say()    // meow ~

參考:
輸入理解js系列
來自ES6入門實踐

相關文章