探索Javascript設計模式---單例模式

最近打算系統的學習javascript設計模式,以便本身在開發中遇到問題能夠按照設計模式提供的思路進行封裝,這樣能夠提升開發效率而且能夠預先規避不少未知的問題。javascript

先從最基本的單例模式開始。html

什麼是單例模式

單例模式,從名字拆分來看,單指的是一個,例是實例,意思是說屢次經過某個類創造出來實例始終只返回同一個實例,它限制一個類只能有一個實例。單例模式主要是爲了解決對象的建立問題。單例模式的特色:java

  1. 一個類只有一個實例設計模式

  2. 對外提供惟一的訪問接口瀏覽器

在一些以類爲核心的語言中,例如java,每建立一個對象就必須先定義一個類,對象是從類建立而來。js是一門無類(class-free)的語言,在js中建立對象的方法很是簡單,不須要先定義類便可建立對象。緩存

在js中,單例模式是一種常見的模式,例如瀏覽器中提供的window對象,處理數字的Math對象。閉包

單例模式的實現

1. 對象字面量

在js中實現單例最簡單的方式是建立對象字面量,字面量對象中能夠包含多個屬性和方法。app

var mySingleton = {
    attr1:1,
    attr2:2,
    method:function (){
        console.log("method");    
    }
}

以上建立一個對象,放在全局中,就能夠在任何地方訪問,要訪問對象中的屬性和方法,必須經過mySingleton這個對象,也就是說提供了惟一一個訪問接口。dom

2. 使用閉包私有化

擴展mySingleton對象,添加私有的屬性和方法,使用閉包的形式在其內部封裝變量和函數聲明,只暴露公共成員和方法。函數

var mySingleton = (function (){
    //私有變量
    var privateVal = '我是私有變量';
    //私有函數
    function privateFunc(){
        console.log('我是私有函數');    
    }

    return {
            attr1:1,
            attr2:2,
            method:function (){
                console.log("method");    
                privateFunc();
            }
        }    
})()

privateValprivateVal被封裝在閉包產生的做用域中,外界訪問不到這兩個變量,這避免了對全局命名污染。

3.惰性單例

不管使用對象字面量或者閉包私有化的方式建立單例,都是在腳本一加載就被建立。有時候頁面可能不會用到這個單例對象,這樣就會形成資源浪費。對於這種狀況,最佳處理方式是使用惰性單例,也就是在須要這個單例對象時再初始化。

var mySingleton = (function (){
    function init(){
        //私有變量
        var privateVal = '我是私有變量';
        //私有函數
        function privateFunc(){
            console.log('我是私有函數');    
        }

        return {
            attr1:1,
            attr2:2,
            method(){
                console.log("method");    
                privateFunc();
            }
        }
    }

    //用來保存建立的單例對象
     var instance = null;
    return {
        getInstance (){
            //instance沒有存值,就執行函數獲得對象
            if(!instance){
                instance = init();
            }    
            //instance存了值,就返回這個對象
            return instance;
        }
    }
})();

//獲得單例對象
var singletonObj1 = mySingleton.getInstance();
var singletonObj2 = mySingleton.getInstance();

console.log( singletonObj1 === singletonObj2 ); //true

程序執行後,將建立單例對象的代碼封裝到init函數中,只暴露了獲取單例對象的函數getInstance。當有須要用到時,經過調用函數mySingleton.getInstance()獲得單例對象,同時使用instance將對象緩存起來,再次調用mySingleton.getInstance()後獲得的是同一個對象,這樣經過一個函數不會建立多個對象,起到節省資源的目的。

4. 使用構造函數

能夠使用構造函數的方式,創造單例對象:

function mySingleton(){
    //若是緩存了實例,則直接返回
    if (mySingleton.instance) {
        return mySingleton.instance;
    }

    //當第一次實例化時,先緩存實例
    mySingleton.instance = this;

}

mySingleton.prototype.otherFunc = function (){
    console.log("原型上其餘方法");    
}

var p1 = new mySingleton();
var p2 = new mySingleton();

console.log( p1 === p2 );  //true

當第一次使用new調用函數建立實例時,經過函數的靜態屬性mySingleton.instance把實例緩存起來,在第二次用new調用函數,判斷實例已經緩存過了,直接返回,那麼第一次獲得的實例p1和第二次獲得的實例p2是同一個對象。這樣符合單例模式的特色:一個類只能有一個實例

這樣作有一個問題,暴露了能夠訪問緩存實例的屬性mySingleton.instance,這個屬性的值能夠被改變:

var p1 = new mySingleton();
//改變mySingleton.instance的值
//mySingleton.instance = null;
//或者
mySingleton.instance = {};
var p2 = new mySingleton();
console.log( p1 === p2 );  //false

改變了mySingleton.instance值後,再經過new調用構造函數建立實例時,又會從新建立新的對象,那麼p1p2就不是同一個對象,違反了單例模式一個類只能有一個實例。

閉包中的實例

不使用函數的靜態屬性緩存實例,而是從新改寫構造函數:

function mySingleton(){
    //緩存當前實例
    var instance  = this;

    //執行完成後改寫構造函數
    mySingleton = function (){
        return instance;    
    }
    //其餘的代碼
    instance.userName = "abc";

}

mySingleton.prototype.otherFunc = function (){
    console.log("原型上其餘方法");    
}

var p1 = new mySingleton();
var p2 = new mySingleton();

console.log( p1 === p2 );  //true

第一次使用new調用函數建立實例後,在函數中建立instance用來緩存實例,把mySingleton改寫爲另外一個函數。若是再次使用new調用函數後,利用閉包的特性,返回了緩存的對象,因此p1p2是同一個對象。

這樣雖然也能夠保證一個類只返回一個實例,但注意,第二次再次使用new調用的構造函數是匿名函數,由於mySingleton已經被改寫:

//第二次new mySingleton()時這個匿名函數纔是真正的構造函數
mySingleton = function (){
    return instance;    
}

再次給原mySingleton.prototype上添加是屬性,實際上這是給匿名函數的原型添加了屬性:

var p1 = new mySingleton();

//再次給mySingleton的原型上添加屬性
mySingleton.prototype.addAttr = "我是新添加的屬性";

var p2 = new mySingleton();

console.log(p2.addAttr); //undefined

對象p2訪問屬性addAttr並無找到。經過一個構造函數構造出來的實例並不能訪問原型上的方法或屬性,這是一種錯誤的作法,還須要繼續改進。

function mySingleton(){
        
    var instance;

    //改寫構造函數
    mySingleton = function (){
        return instance;    
    }

    //把改寫後構造函數的原型指向this
    
    mySingleton.prototype = this;

    //constructor改寫爲改寫後的構造函數
    mySingleton.prototype.constructor = mySingleton;

    //獲得改寫後構造函數建立的實例
    instance = new mySingleton;

    //其餘的代碼
    instance.userName = "abc";

    //顯示的返回改寫後構造函數建立的實例
    return instance;

}

mySingleton.prototype.otherFunc = function (){
    console.log("原型上其餘方法");    
}

var p1 = new mySingleton();

//再次給mySingleton的原型上添加屬性
mySingleton.prototype.addAttr = "我是新添加的屬性";

var p2 = new mySingleton();

console.log(p2.addAttr); //'我是新添加的屬性'

console.log( p1 === p2 );  //true

以上代碼主要作了如下幾件事:

  1. 改寫mySingleton函數爲匿名函數

  2. 改寫mySingleton的原型爲第一次經過new建立的實例

  3. 由於改寫了prototype,要把constructor指回mySingleton

  4. 顯式返回經過改寫後mySingleton構造函數構造出的實例

不管使用多少次new調用mySingleton這個構造函數,都返回同一個對象,而且這些對象都共享同一個原型。

實踐單例模式

1. 使用命名空間

根據上述,在js中建立一個對象就是一個單例,把一類的方法和屬性放在對象中,都經過提供的全局對象訪問。

var mySingleton = {
    attr1:1,
    attr2:2,
    method:function (){
        console.log("method");    
    }
}

這樣的方式耦合度極高,例如:要給這個對象添加屬性:

mySingleton.width = 1000;  //添加一個屬性

//添加一個方法會覆蓋原有的方法
mySingleton.method = function(){};

若是在多人協做中,這樣添加屬性的方式常常出現被覆蓋的危險,能夠採用命名空間的方式解決。

//A同窗
mySingleton.a = {};
mySingleton.a.method = function(){}
//訪問
mySingleton.a.method();

//B同窗
mySingleton.b = {};
mySingleton.b.method = function(){}
//訪問
mySingleton.b.method();

都在本身的命名空間中,覆蓋的概率會很小。
能夠封裝一個動態建立命名空間的通用方法,這樣在須要獨立的命名空間時只須要調用函數便可。

mySingleton.namespace = function(name){
    var arr = name.split(".");
    //存一下對象
    var currentObj = mySingleton;
    for( var i = 0; i < arr.length; i++ ){
        //若是對象中不存在,則賦值添加屬性
        if(!currentObj[arr[i]]){
            currentObj[arr[i]] = {};
        }
        //把變量從新賦值,便於循環繼續建立命名空間
        currentObj = currentObj[arr[i]]
    }
}

//建立命名空間
mySingleton.namespace("bom");
mySingleton.namespace("dom.style");

以上調用函數生成命名空間的方式代碼等價於:

mySingleton.bom = {};
mySingleton.dom = {};
mySingleton.dom.style = {};

2. 單例登陸框

使用面向對象實現一個登陸框,在點擊登陸按鈕後登陸框被append到頁面中,點擊關閉就將登陸框從頁面中remove掉,這樣頻繁的操做DOM不合理也不是必要的。

只須要在點擊關閉時隱藏登陸框,再次點擊按鈕後,只須要show出來便可。

頁面中只放一個按鈕:

<input type="button" value="登陸" id="loginBtn" />

js實現:

function Login(){
    var instance;

    Login = function(){
        return install;
    }

    Login.prototype = this;


    install = new Login;

    install.init();

    return install;
}

Login.prototype.init = function(){
    //獲得登陸框元素
    this.Login = this.createHtml();
    document.body.appendChild(this.Login);
    //綁定事件
    this.addEvent();
}
Login.prototype.createHtml = function(){
    var LoginDiv = document.createElement("div");
    LoginDiv.className = "box";
    var html = `<input type="button" value="關閉彈框" class="close" /><p>這裏作登陸</p>`

    LoginDiv.innerHTML = html;

    return LoginDiv;
}
Login.prototype.addEvent = function(){
    var close = this.Login.querySelector(".close");
    var _this = this;
    close.addEventListener("click",function(){
        _this.Login.style.display = 'none';
    })
}
Login.prototype.show = function(){
    this.Login.style.display = 'block';
}
//點擊頁面中的按鈕
var loginBtn = document.querySelector("#loginBtn");
loginBtn.onclick = function(){
    var login = new Login();
    //每次讓登陸框出現便可
    login.show();
}

上面的代碼根據單例模式的使用構造函數來實現的。這樣在一開始生成了一個對象,以後使用的都是同一個對象。

總結

單例模式是一種很是實用的模式,特別是懶性單例技術,在合適時候建立對象,而且只建立惟一一個,這樣減小沒必要要的內存消耗。


正在學習設計模式,不正確的地方歡迎拍磚指正。

相關文章
相關標籤/搜索