最近打算系統的學習javascript設計模式,以便本身在開發中遇到問題能夠按照設計模式提供的思路進行封裝,這樣能夠提升開發效率而且能夠預先規避不少未知的問題。javascript
先從最基本的單例模式開始。html
單例模式,從名字拆分來看,單指的是一個,例是實例,意思是說屢次經過某個類創造出來實例始終只返回同一個實例,它限制一個類只能有一個實例。單例模式主要是爲了解決對象的建立問題。單例模式的特色:java
一個類只有一個實例設計模式
對外提供惟一的訪問接口瀏覽器
在一些以類爲核心的語言中,例如java,每建立一個對象就必須先定義一個類,對象是從類建立而來。js是一門無類(class-free)的語言,在js中建立對象的方法很是簡單,不須要先定義類便可建立對象。緩存
在js中,單例模式是一種常見的模式,例如瀏覽器中提供的window對象,處理數字的Math對象。閉包
在js中實現單例最簡單的方式是建立對象字面量,字面量對象中能夠包含多個屬性和方法。app
var mySingleton = { attr1:1, attr2:2, method:function (){ console.log("method"); } }
以上建立一個對象,放在全局中,就能夠在任何地方訪問,要訪問對象中的屬性和方法,必須經過mySingleton這個對象,也就是說提供了惟一一個訪問接口。dom
擴展mySingleton對象,添加私有的屬性和方法,使用閉包的形式在其內部封裝變量和函數聲明,只暴露公共成員和方法。函數
var mySingleton = (function (){ //私有變量 var privateVal = '我是私有變量'; //私有函數 function privateFunc(){ console.log('我是私有函數'); } return { attr1:1, attr2:2, method:function (){ console.log("method"); privateFunc(); } } })()
把privateVal和privateVal被封裝在閉包產生的做用域中,外界訪問不到這兩個變量,這避免了對全局命名污染。
不管使用對象字面量或者閉包私有化的方式建立單例,都是在腳本一加載就被建立。有時候頁面可能不會用到這個單例對象,這樣就會形成資源浪費。對於這種狀況,最佳處理方式是使用惰性單例,也就是在須要這個單例對象時再初始化。
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()後獲得的是同一個對象,這樣經過一個函數不會建立多個對象,起到節省資源的目的。
能夠使用構造函數的方式,創造單例對象:
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這個構造函數,都返回同一個對象,而且這些對象都共享同一個原型。
根據上述,在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 = {};
使用面向對象實現一個登陸框,在點擊登陸按鈕後登陸框被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(); }
上面的代碼根據單例模式的使用構造函數來實現的。這樣在一開始生成了一個對象,以後使用的都是同一個對象。
單例模式是一種很是實用的模式,特別是懶性單例技術,在合適時候建立對象,而且只建立惟一一個,這樣減小沒必要要的內存消耗。
正在學習設計模式,不正確的地方歡迎拍磚指正。