最近打算系統的學習 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 = 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
關注妙味訂閱號:“妙味前端”,為您帶來優質前端技術乾貨;