單例模式是一種十分經常使用但卻相對而言比較簡單的單例模式。它是指在一個類只能有一個實例,即便屢次實例化該類,也只返回第一次實例化後的實例對象。單例模式不只能減小沒必要要的內存開銷, 而且在減小全局的函數和變量衝突也具備重要的意義。程序員
就算你對於單例模式的概念還比較模糊,可是我相信你確定已經使用過單例模式了。咱們來看一下下面的一段代碼:ajax
let timeTool = { name: '處理時間工具庫', getISODate: function() {}, getUTCDate: function() {} }
以對象字面量建立對象的方式在JS開發中很常見。上面的對象是一個處理時間的工具庫, 以對象字面量的方式來封裝了一些方法處理時間格式。全局只暴露了一個timeTool
對象, 在須要使用時, 只須要採用timeTool.getISODate()
調用便可。timeTool
對象就是單例模式的體現。在JavaScript建立對象的方式十分靈活, 能夠直接經過對象字面量的方式實例化一個對象, 而其餘面向對象的語言必須使用類進行實例化。因此,這裏的timeTool
就已是一個實例, 且ES6中let
和const
不容許重複聲明的特性,確保了timeTool
不能被從新覆蓋。api
採用對象字面量建立單例只能適用於簡單的應用場景,一旦該對象十分複雜,那麼建立對象自己就須要必定的耗時,且該對象可能須要有一些私有變量和私有方法。此時使用對象字面建立單例就再也不行得通了,咱們仍是須要採用構造函數的方式實例化對象。下面就是使用當即執行函數和構造函數的方式改造上面的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
做爲實例對象最開始賦值爲null
,init
函數是其構造函數,用於實例化對象,當即執行函數返回的是匿名函數用於判斷實例是否建立,只有當調用timeTool()
時進行實例的實例化,這就是惰性單例的應用,不在js加載時就進行實例化建立, 而是在須要的時候再進行單例的建立。 若是再次調用, 那麼返回的永遠是第一次實例化後的實例對象。app
let instance1 = timeTool(); let instance2 = timeTool(); console.log(instance1 === instance2); //true
一個項目經常不僅一個程序員進行開發和維護, 而後一個程序員很難去弄清楚另外一個程序員暴露在的項目中的全局變量和方法。若是將變量和方法都暴露在全局中, 變量衝突是在所不免的。就想下面的故事同樣: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();
上面代碼中,devA
和devB
就是兩個命名空間,採用命名空間能夠有效減小全局變量的數量,以此解決變量衝突的發生。工具
上面說到的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
,dom
和event
三個模塊,用同一個命名空間devA
來管理。在進行相應操做的時候,只須要devA.ajax.get()
進行調用便可。這樣可讓庫的功能更加清晰。優化
ES6中建立對象時引入了class
和constructor
用來建立對象。下面咱們來使用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']);
蘋果這麼偉大的公司明顯有且只有一個, 就是喬爺爺建立的那個, 哪能容別人進行復制?因此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
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
登錄彈框在項目中是一個比較經典的單例模式,由於對於大部分網站不須要用戶必須登錄才能瀏覽,因此登錄操做的彈框能夠在用戶點擊登錄按鈕後再進行建立。並且登錄框永遠只有一個,不會出現多個登錄彈框的狀況,也就意味着再次點擊登錄按鈕後返回的永遠是一個登陸框的實例。
如今來梳理一下我登錄彈框的流程,在來進行代碼的實現:
由於5,6是登錄框的實際項目邏輯, 和單例模式關係不大。下面的項目實戰代碼只實現1 - 4步,其他步驟讀者可自行進行擴展練習。完整的代碼可在 CodePen中進行查看。
<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>
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; } }
//註冊點擊事件 Login.getLoginDom('.login-btn').addEventListener('click', () => { Login.getInstance(); })
完整的項目代碼見: CodePen(單例模式案例——登陸框)
上面的登錄框的實現中,咱們只建立了一個Login
的類, 可是卻實現了一個並不簡單的登錄功能。在第一次點擊登錄按鈕的時候,咱們調用Login.getInstance()
實例化了一個登錄框,且在以後的點擊中,並無從新建立新的登錄框,只是移除掉了"display: none"
這個樣式來顯示登錄框,節省了內存開銷。
單例模式雖然簡單,可是在項目中的應用場景倒是至關多的,單例模式的核心是確保只有一個實例, 並提供全局訪問。就像咱們只須要一個瀏覽器的window
對象, jQuery的$
對象而再也不須要第二個。 因爲JavaScript代碼書寫方式十分靈活, 這也致使了若是沒有嚴格的規範的狀況下,大型的項目中JavaScript不利於多人協同開發, 使用單例模式進行命名空間,管理模塊是一個很好的開發習慣,可以有效的解決協同開發變量衝突的問題。靈活使用單例模式,也可以減小沒必要要的內存開銷,提升用於體驗。