從ES6從新認識JavaScript設計模式(一): 單例模式

1. 什麼是單例模式?

單例模式是一種十分經常使用但卻相對而言比較簡單的單例模式。它是指在一個類只能有一個實例,即便屢次實例化該類,也只返回第一次實例化後的實例對象。單例模式不只能減小沒必要要的內存開銷, 而且在減小全局的函數和變量衝突也具備重要的意義。程序員

1.1 最簡單的單例模式

就算你對於單例模式的概念還比較模糊,可是我相信你確定已經使用過單例模式了。咱們來看一下下面的一段代碼:ajax

let timeTool = {
  name: '處理時間工具庫',
  getISODate: function() {},
  getUTCDate: function() {}
}

以對象字面量建立對象的方式在JS開發中很常見。上面的對象是一個處理時間的工具庫, 以對象字面量的方式來封裝了一些方法處理時間格式。全局只暴露了一個timeTool對象, 在須要使用時, 只須要採用timeTool.getISODate()調用便可。timeTool對象就是單例模式的體現。在JavaScript建立對象的方式十分靈活, 能夠直接經過對象字面量的方式實例化一個對象, 而其餘面向對象的語言必須使用類進行實例化。因此,這裏的timeTool就已是一個實例, 且ES6中letconst不容許重複聲明的特性,確保了timeTool不能被從新覆蓋。api

1.2 惰性單例

採用對象字面量建立單例只能適用於簡單的應用場景,一旦該對象十分複雜,那麼建立對象自己就須要必定的耗時,且該對象可能須要有一些私有變量和私有方法。此時使用對象字面建立單例就再也不行得通了,咱們仍是須要採用構造函數的方式實例化對象。下面就是使用當即執行函數和構造函數的方式改造上面的timeTool工具庫。瀏覽器

let timeTool = (function() {
  let _instance = null;
  
  function init() {
    //私有變量
    let now = new Date();
    //公用屬性和方法
    this.name = '處理時間工具庫',
    this.getISODate = function() {
      return now.toISOString();
    }
    this.getUTCDate = function() {
      return now.toUTCString();
    }
  }
  
  return function() {
    if(!_instance) {
      _instance = new init();
    }
    return _instance;
  }
})()

上面的timeTool其實是一個函數,_instance做爲實例對象最開始賦值爲nullinit函數是其構造函數,用於實例化對象,當即執行函數返回的是匿名函數用於判斷實例是否建立,只有當調用timeTool()時進行實例的實例化,這就是惰性單例的應用,不在js加載時就進行實例化建立, 而是在須要的時候再進行單例的建立。 若是再次調用, 那麼返回的永遠是第一次實例化後的實例對象。app

let instance1 = timeTool();
let instance2 = timeTool();
console.log(instance1 === instance2); //true

2. 單例模式的應用場景

2.1 命名空間

一個項目經常不僅一個程序員進行開發和維護, 而後一個程序員很難去弄清楚另外一個程序員暴露在的項目中的全局變量和方法。若是將變量和方法都暴露在全局中, 變量衝突是在所不免的。就想下面的故事同樣:dom

//開發者A寫了一大段js代碼
function addNumber () {}


//開發者B開始寫js代碼
var addNumber = '';

//A從新維護該js代碼
addNumber(); //Uncaught TypeError: addNumber is not a function

命名空間就是用來解決全局變量衝突的問題,咱們徹底能夠只暴露一個對象名,將變量做爲該對象的屬性,將方法做爲該對象的方法,這樣就能大大減小全局變量的個數。函數

//開發者A寫了一大段js代碼
let devA = {
  addNumber() { }
}

//開發者B開始寫js代碼
let devB = {
  add: ''
}

//A從新維護該js代碼
devA.addNumber();

上面代碼中,devAdevB就是兩個命名空間,採用命名空間能夠有效減小全局變量的數量,以此解決變量衝突的發生。工具

2.2 管理模塊

上面說到的timeTool對象是一個只用來處理時間的工具庫,可是實際開發過程當中的庫可能會有多種多樣的功能,例如處理ajax請求,操做dom或者處理事件。這個時候單例模式還能夠用來管理代碼庫中的各個模塊,例以下面的代碼所示。post

var devA = (function(){
  //ajax模塊
  var ajax = {
    get: function(api, obj) {console.log('ajax get調用')},
    post: function(api, obj) {}
  }

  //dom模塊
  var dom = {
    get: function() {},
    create: function() {}
  }
  
  //event模塊
  var event = {
    add: function() {},
    remove: function() {}
  }

  return {
    ajax: ajax,
    dom: dom,
    event: event
  }
})()

上面的代碼庫中有ajax,domevent三個模塊,用同一個命名空間devA來管理。在進行相應操做的時候,只須要devA.ajax.get()進行調用便可。這樣可讓庫的功能更加清晰。優化

3. ES6中的單例模式

3.1 ES6建立對象

ES6中建立對象時引入了classconstructor用來建立對象。下面咱們來使用ES6的語法實例化蘋果公司

class Apple {
  constructor(name, creator, products) {
    this.name = name;
    this.creator = creator;
    this.products = products;
  }
}

let appleCompany = new Apple('蘋果公司', '喬布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new Apple('蘋果公司', '阿輝', ['iPhone', 'iMac', 'iPad', 'iPod']);

3.2 ES6中建立單例模式

蘋果這麼偉大的公司明顯有且只有一個, 就是喬爺爺建立的那個, 哪能容別人進行復制?因此appleCompany應該是一個單例, 如今咱們使用ES6的語法將constructor改寫爲單例模式的構造器。

class SingletonApple {
  constructor(name, creator, products) {
    //首次使用構造器實例
    if (!SingletonApple.instance) {
      this.name = name;
      this.creator = creator;
      this.products = products;
      //將this掛載到SingletonApple這個類的instance屬性上
      SingletonApple.instance = this;
    }
    return SingletonApple.instance;
  }
}

let appleCompany = new SingletonApple('蘋果公司', '喬布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = new SingletonApple('蘋果公司', '阿輝', ['iPhone', 'iMac', 'iPad', 'iPod']);

console.log(appleCompany === copyApple);  //true

3.3 ES6的靜態方法優化代碼

ES6中提供了爲class提供了static關鍵字定義靜態方法, 咱們能夠將constructor中判斷是否實例化的邏輯放入一個靜態方法getInstance中,調用該靜態方法獲取實例, constructor中只包需含實例化所需的代碼,這樣能加強代碼的可讀性、結構更加優化。

class SingletonApple {
  constructor(name, creator, products) {
      this.name = name;
      this.creator = creator;
      this.products = products;
  }
  //靜態方法
  static getInstance(name, creator, products) {
    if(!this.instance) {
      this.instance = new SingletonApple(name, creator, products);
    }
    return this.instance;
  }
}

let appleCompany = SingletonApple.getInstance('蘋果公司', '喬布斯', ['iPhone', 'iMac', 'iPad', 'iPod']);
let copyApple = SingletonApple.getInstance('蘋果公司', '阿輝', ['iPhone', 'iMac', 'iPad', 'iPod'])

console.log(appleCompany === copyApple); //true

4. 單例模式的項目實戰應用

4.1 實現登錄彈框

登錄彈框在項目中是一個比較經典的單例模式,由於對於大部分網站不須要用戶必須登錄才能瀏覽,因此登錄操做的彈框能夠在用戶點擊登錄按鈕後再進行建立。並且登錄框永遠只有一個,不會出現多個登錄彈框的狀況,也就意味着再次點擊登錄按鈕後返回的永遠是一個登陸框的實例。

如今來梳理一下我登錄彈框的流程,在來進行代碼的實現:

  1. 給頂部導航模塊的登錄按鈕註冊點擊事件
  2. 登錄按鈕點擊後JS動態建立遮罩層和登錄彈框
  3. 遮罩層和登錄彈框插入到頁面中
  4. 給登錄框中的關閉按鈕註冊事件, 用於關閉遮罩層和彈框
  5. 給登錄框中的輸入框添加校驗(此步驟略)
  6. 給登錄框中的肯定按鈕添加事件,用於Ajax請求(此步驟略)
  7. 給登錄框中的清空按鈕添加事件,用於清空輸入框(此步驟略)

由於5,6是登錄框的實際項目邏輯, 和單例模式關係不大。下面的項目實戰代碼只實現1 - 4步,其他步驟讀者可自行進行擴展練習。完整的代碼可在 CodePen中進行查看。

4.1.1 給頁面添加頂部導航欄的HTML代碼

<nav class="top-bar">
    <div class="top-bar_left">
      LTH BLOG
    </div>
    <div class="top-bar_right">
      <div class="login-btn">登錄</div>
      <div class="signin-btn">註冊</div>
    </div>
  </nav>

4.1.2 使用ES6的語法建立Login類

class Login {

  //構造器
  constructor() {
    this.init();
  }

  //初始化方法
  init() {
    //新建div
    let mask = document.createElement('div');
    //添加樣式
    mask.classList.add('mask-layer');
    //添加模板字符串
    mask.innerHTML = 
    `
    <div class="login-wrapper">
      <div class="login-title">
        <div class="title-text">登陸框</div>
        <div class="close-btn">×</div>
      </div>
      <div class="username-input user-input">
        <span class="login-text">用戶名:</span>
        <input type="text">
      </div>
      <div class="pwd-input user-input">
        <span class="login-text">密碼:</span>
        <input type="password">
      </div>
      <div class="btn-wrapper">
        <button class="confrim-btn">肯定</button>
        <button class="clear-btn">清空</button>
      </div>
    </div>
    `;
    //插入元素
    document.body.insertBefore(mask, document.body.childNodes[0]);

    //註冊關閉登陸框事件
    Login.addCloseLoginEvent();
  }

  //靜態方法: 獲取元素
  static getLoginDom(cls) {
    return  document.querySelector(cls);
  }

  //靜態方法: 註冊關閉登陸框事件
  static addCloseLoginEvent() {
    this.getLoginDom('.close-btn').addEventListener('click', () => {
      //給遮罩層添加style, 用於隱藏遮罩層
      this.getLoginDom('.mask-layer').style = "display: none";
    })
  }

  //靜態方法: 獲取實例(單例)
  static getInstance() {
    if(!this.instance) {
      this.instance = new Login();
    } else {
      //移除遮罩層style, 用於顯示遮罩層
      this.getLoginDom('.mask-layer').removeAttribute('style');
    }
    return this.instance;
  }
}

4.1.3 給登錄按鈕添加註冊點擊事件

//註冊點擊事件
Login.getLoginDom('.login-btn').addEventListener('click', () => {
  Login.getInstance();
})

4.1.4 效果演示

單例效果展現.gif-36.8kB

完整的項目代碼見: CodePen(單例模式案例——登陸框)

上面的登錄框的實現中,咱們只建立了一個Login的類, 可是卻實現了一個並不簡單的登錄功能。在第一次點擊登錄按鈕的時候,咱們調用Login.getInstance()實例化了一個登錄框,且在以後的點擊中,並無從新建立新的登錄框,只是移除掉了"display: none"這個樣式來顯示登錄框,節省了內存開銷。

總結

單例模式雖然簡單,可是在項目中的應用場景倒是至關多的,單例模式的核心是確保只有一個實例, 並提供全局訪問。就像咱們只須要一個瀏覽器的window對象, jQuery的$對象而再也不須要第二個。 因爲JavaScript代碼書寫方式十分靈活, 這也致使了若是沒有嚴格的規範的狀況下,大型的項目中JavaScript不利於多人協同開發, 使用單例模式進行命名空間,管理模塊是一個很好的開發習慣,可以有效的解決協同開發變量衝突的問題。靈活使用單例模式,也可以減小沒必要要的內存開銷,提升用於體驗。

相關文章
相關標籤/搜索