單例模式 | 程式設計師都想要探索的 Javascript 設計模

小橋流水嘩啦啦發表於2018-10-25

最近打算系統的學習 Javascript

設計模式,以便自己在開發中遇到問題可以按照設計模式提供的思路進行封裝,這樣可以提高開發效率並且可以預先規避很多未知的問題。

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

什麼是單例模式

單例模式,從名字拆分來看,單指的是一個,例是例項,意思是說多次通過某個類創造出來例項始終只返回同一個例項,它限制一個類只能有一個例項。單例模式主要是為了解決物件的建立問題。單例模式的特點:

1、一個類只有一個例項

2、對外提供唯一的訪問介面

在一些以類為核心的語言中,例如java,每建立一個物件就必須先定義一個類,物件是從類建立而來。js是一門無類(class-free)的語言,在js中建立物件的方法非常簡單,不需要先定義類即可建立物件。

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

單例模式的實現

1

物件字面量

在js中實現單例最簡單的方式是建立物件字面量,字面量物件中可以包含多個屬性和方法。

var mySingleton = {

attr1:1,

attr2:2,

method:function (){

    console.log("method");  
    
}
複製程式碼

}

以上建立一個物件,放在全域性中,就可以在任何地方訪問,要訪問物件中的屬性和方法,必須通過mySingleton這個物件,也就是說提供了唯一一個訪問介面。

2

使用閉包私有化

擴充套件mySingleton物件,新增私有的屬性和方法,使用閉包的形式在其內部封裝變數和函式宣告,只暴露公共成員和方法。

var mySingleton = (function (){

 //私有變數
複製程式碼

var privateVal = '我是私有變數';

//私有函式
複製程式碼

function privateFunc(){

    console.log('我是私有函式'); 
複製程式碼

}

return {   

        attr1:1,  
        
        attr2:2,  
        
        method:function (){                                                                   console.log("method");        
                   privateFunc();
       }
   }    
複製程式碼

})()

把privateVal和privateVal被封裝在閉包產生的作用域中,外界訪問不到這兩個變數,這避免了對全域性命名汙染。

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呼叫建構函式建立例項時,又會重新建立新的物件,那麼p1和p2就不是同一個物件,違反了單例模式一個類只能有一個例項。

閉包中的例項

不使用函式的靜態屬性快取例項,而是重新改寫建構函式:

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呼叫函式後,利用閉包的特性,返回了快取的物件,所以p1和p2是同一個物件。

這樣雖然也可以保證一個類只返回一個例項,但注意,第二次再次使用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

以上程式碼主要做了以下幾件事:

數字列表改寫 mySingleton 函式為匿名函式

改寫 mySingleton 的原型為第一次通過 new 建立例項

因為改寫了 prototype,要把 constructor 指回mySingleton

顯式返回通過改寫後 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出來即可。 頁面中只放一個按鈕:

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();
複製程式碼

}

上面的程式碼根據單例模式的使用建構函式來實現的。這樣在一開始生成了一個物件,之後使用的都是同一個物件。

總結

單例模式是一種非常實用的模式,特別是懶性單例技術,在合適時候建立物件,並且只建立唯一一個,這樣減少不必要的記憶體消耗。

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

訂閱號ID:Miaovclass

關注妙味訂閱號:“妙味前端”,為您帶來優質前端技術乾貨;

相關文章