JS 單例模式

SHERlocked93發表於2017-12-14

1. 單例模式

單例模式 (Singleton) 的實現在於保證一個特定類只有一個例項,第二次使用同一個類建立新物件的時候,應該得到與第一次建立物件完全相同的物件。
當建立一個新物件時,實際上沒有其他物件與其類似,因為新物件已經是單例了 {a:1} === {a:1} // false

但是如何在對建構函式使用 new 操作符建立多個物件的時候僅獲取一個單例物件呢。

2. 靜態屬性中的例項

在建構函式的靜態屬性中快取該例項,缺點在於 instance 屬性是公開可訪問的屬性,在外部程式碼中可能會修改該屬性。

function Universe() {
    if (typeof Universe.instance === 'object') {        // 判斷是否已經有單例了
        return Universe.instance
    }
    Universe.instance = this
    return this
}
var uni1 = new Universe()
var uni2 = new Universe()
uni1 === uni2            // true

3. 閉包中的例項

可以把例項封裝在閉包中,這樣可以保證該例項的私有性並且保證該例項不會在建構函式之外被修改,代價是帶來了額外的閉包開銷。

function Universe() {
    var instance = this
    Universe = function() {    // 重寫建構函式
        return instance
    }
}
var uni1 = new Universe()
var uni2 = new Universe()
uni1 === uni2         // true

當第一次呼叫建構函式時,它正常返回 this ,然後在以後呼叫時,它將會執行重寫建構函式,這個建構函式通過閉包訪問了私有 instance 變數,並且簡單的返回了該 instance

4. 惰性單例

有時候對於單例物件需要延遲建立,所以在單例中還存在一種延遲建立的形式,也有人稱之為惰性建立

const LazySingle = (function() {
  let _instance              // 單例的例項引用
 
  function Single() {        // 單例建構函式
    const desc = '單例'        // 私有屬性和方法
    return {                   // 暴露出來的物件
      publicMethod: function() {console.log(desc)},
      publickProperty: '1.0'
    }
  }
  
  return function() {
    return _instance || (_instance = Single())
  }
})()

console.log(LazySingle()===lazySingle())        // true
console.log(LazySingle().publickProperty)       // 1.0

5. 改進

之前在建構函式中重寫自身會丟失所有在初始定義和重定義之間新增到其中的屬性。在這種情況下,任何新增到 Universe() 的原型中的物件都不會存在指向由原始實現所建立例項的活動連結:

function Universe() {
    var instance = this
    Universe = function() {
        return instance
    }
}
Universe.prototype.nothing = true
var uni1 = new Universe()
Universe.prototype.enthing = true
var uni2 = new Universe()
console.log(uni1 === uni2) // true

uni1.nothing // true
uni2.nothing // true
uni1.enthing // undefined
uni2.enthing // undefined
uni1.constructor.name // "Universe"
uni1.constructor === Universe // false

之所以 uni1.constructor 不再與 Universe() 相同,是因為uni1.constructor仍然指向原始的建構函式,而不是重定義之後的那個建構函式。
可以通過一些調整實現原型和建構函式指標按照預期的那樣執行:

function Universe() {
    var instance
    Universe = function Universe() {
        return instance
    }
    Universe.prototype = this // 保留原型屬性
    instance = new Universe()
    instance.constructor = Universe // 重置建構函式指標
    instance.start_time = 0 // 一些屬性
    instance.big = 'yeah'
    return instance
}
Universe.prototype.nothing = true
var uni1 = new Universe()
Universe.prototype.enthing = true
var uni2 = new Universe()
console.log(uni1 === uni2) // true

uni1.nothing & uni2.nothing & uni1.enthing & uni2.enthing // true
uni1.constructor.name // "Universe"
uni1.constructor === Universe // true
uni1.big    // "yeah"
uni2.big    // "yeah"

本文是系列文章,可以相互參考印證,共同進步~

  1. JS 抽象工廠模式
  2. JS 工廠模式
  3. JS 建造者模式
  4. JS 原型模式
  5. JS 單例模式
  6. JS 回撥模式
  7. JS 外觀模式
  8. JS 介面卡模式
  9. JS 利用高階函式實現函式快取(備忘模式)
  10. JS 狀態模式
  11. JS 橋接模式
  12. JS 觀察者模式

網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

參考:
《JavaScript模式》 P143
《Javascript 設計模式》 - 張榮銘
設計模式之單例模式

相關文章