JavaScript中單例模式這樣用

一顆冰淇淋發表於2023-03-01
如果希望自己的程式碼更優雅、可維護性更高以及更簡潔,往往離不開設計模式這一解決方案。

在JS設計模式中,最核心的思想:封裝變化(將變與不變分離,確保變化的部分靈活,不變的部分穩定)。

單例模式

那麼來說說第一個常見的設計模式:單例模式

單例模式保證一個類僅有一個例項,並提供一個訪問它的全域性訪問方式,為了解決一個全域性使用的類頻繁被建立和銷燬、佔用記憶體的問題。

ES5中透過閉包

在ES5中,可以使用閉包(函式內部返回函式被外界變數所引用,導致這個函式里面的變數無法被釋放,就構建成閉包)來儲存這個類的例項。

var Singeton = (function(){
    var instance;
    
    function User(name,age){
        this.name=name;
        this.age=age;
    }
    
    return function(name,age){
        if(!instance){
            instance = new User(name,age)
        }
        return instance
    }
})()

此時這個例項一旦生成,每次都是返回這個例項,且不會被修改,可以看到下面的程式碼,當給 User 物件初始賦值 name:alice,age:18 時,以後再賦值便無效了,以及每次返回都是初始的例項物件。

ES6中使用類的靜態屬性

以上程式碼使用ES6語法來實現,透過類的靜態屬性來儲存唯一的例項物件。

  class Singeton {
    constructor(name,age){
        if(!Singeton.instance){
            this.name = name;
            this.age = age;
            Singeton.instance = this;
        }

       return Singeton.instance;
    }
}

建立方式仍然是一樣的,透過 new 關鍵字建立類的例項物件。

案例

那這樣一種設計模式在開發中實際有什麼用途呢?我們試想這樣一個業務場景:訪問網站時,很久沒有操作頁面,此時授權過期,當我們點選頁面上的任何一個地方,都會彈出一個登入框。

那麼這個登入框,是全域性唯一的,不會存在多份,也不會互相沖突,所以不需要每次都建立一份,保留初始那一份就夠了。

提前建立節點

我們可能會想到首先在頁面中提前建立節點,編寫好頁面樣式,最後透過控制元素的 display 屬性來達到顯示和隱藏的效果。

<div class="modal">登入對話方塊</div>
<button id="open">開啟</button>
<button id="close">關閉</button>

<style>
  .modal {
    display: none;
    /* 其他佈局程式碼省略 */
  }
</style>

<script>
  document.querySelector("#open").onclick = function(){
     const modal = document.querySelector('.modal')
     modal.style.display = 'block'
  }
</script>

這樣可以完成需求,全域性只有一個登入框,每次都展示同一個。但問題是dom元素從一開始它建立好並新增到body中,無論是否需要用到,如果有些場景不需要登陸,那麼這裡初始渲染就會浪費空間。

單例模式

那如果不需要初始渲染,僅當需要時才使用,並且每次都返回同一個例項的單例模式應該如何實現呢?

我們可以這樣處理

<!-- 去除class為modal的標籤,動態建立 -->

<script>
const Modal = (function(){
  let instance = null
  return function(){
      if(!instance){
          instance = document.createElement("div")
          instance.innerHTML = "登入對話方塊"
          instance.className = "modal"
          instance.style.display = "none"
          document.body.appendChild(instance)
      }
      return instance
  }
})()

document.querySelector("#open").onclick = function(){
      //建立modal,如果放在外面,一開始就會建立元素
      const modal = Modal()    
      //顯示modal
      modal.style.display = "block"
  }

  document.querySelector("#close").onclick = function(){
      const modal = Modal()    
      modal.style.display = "none"
  }
</script>

雖然上面的方式可以達到效果,但是建立物件和管理單例的邏輯都放在了物件內部,是有些混亂的。並且如果下次需要建立頁面中唯一的 iframe,或者 script 標籤,就得將以上函式照抄一遍。

通用單例

首先拆分函式邏輯,將執行建立物件的邏輯拿出來

const createLayer = function(){
  let div = document.createElement("div")
  div.innerHTML = "登入對話方塊"
  div.className = "modal"
  div.style.display = "none"
  document.body.appendChild(div);
  return div;
}

const Modal = (function(){
  let instance = null
  return function(){
      if(!instance){
          instance = createLayer()
      }
      return instance
  }
})()

以上修改後程式碼邏輯就更為清晰,但此時還不支援通用化的建立別的元件,這時候我們想想如何將建立單例的方法進行一些最佳化,是否可以將單例需要執行的函式抽象化。

const createSingle = (function(fn){
    let instance; 
    return function(){
        return instance || ( instance = fn.apply(this, arguments))
    }
})()

這樣改造後,如果存在建立 iframe 的方法,也可以直接使用。

const createIframe = function() {
  const iframe = document.createElement('iframe');
  iframe.style.display = 'none';
  document.body.appendChild(iframe);
  return iframe
}
const singleIframe = createSingle(createIframe)

document.querySelector("#open").onclick = function(){
 const iframe = singleIframe()
 iframe.style.display = 'block'
}

實際應用

以上都是我們小打小鬧的試用,那再來看看社群中一些非常棒的實現吧~ 比如:React 中常用的狀態管理工具 Redux 就使用到了單例模式,它有這樣一些要求。

  • 單一資料來源:整個應用的 state 只存在於唯一一個 store 中。
  • State 是隻讀的:不要直接改變 state 的值,唯一改變 state 的方法就是觸發 action。
  • reducer 是純函式:需要編寫純函式 reducer 來修改 state 的值。

來看看 Redux 的原始碼,為了便於閱讀已刪減部分邏輯判斷和註釋,可以看到透過 store 的 getState 方法每次獲取閉包中的 currentState。

單例模式在記憶體中只有一個例項,可以減少記憶體開支,同時還能在系統設定全域性的訪問點,最佳化和共享資源。

以上就是單例模式的相關介紹。更多有關 前端設計模式 的內容可以參考我其它的博文,持續更新中~

相關文章