單體模式作為一種軟件開發模式在眾多面向對象語言中得到了廣泛的使用,在javascript中,單體模式也是使用非常廣泛的,但是由於javascript語言擁有其獨特的面向對象方式,導致其和一些傳統面向對象語言雖然在單體模式的思想上是一致的,但是實現起來還是有差異的。
首先來看看傳統面向對象語言對於單體模式的定義:單體模式是只能被實例化一次並且可以通過一個眾所周知的訪問點來訪問的類。這個定義有兩點突出了傳統面向對象語言的特征,即類和實例化,所以對於傳統面向對象語言來講,單體模式是建立在其類和實例化的自然特性之上的,即使用關鍵字class定義一個類,該類可通過new關鍵字來實例化,但是需要保證每次被new實例化之後得到的都是同一個實例或者說只能通過new來調用其構造函數一次。
再來看看javascript中對於單體模式的定義:單體是一個用來劃分命名空間並將一批相關方法和屬性組織在一起的對象,如果它能夠被實例化,那麼只能被實例化一次。對比上面的定義,你會發現這裡的單體定義將其實質定義為對象,而不是傳統面向對象語言中的類,這也表明了javascript這門語言是基於對象的。同時後面又指出,如果能夠被實例化,這說明了在javascript中單體定義應該有好幾種方式,存在一種或幾種能夠被實例化即使用new關鍵字來創建單體對象的方式,但是這種方式不是javascript自身的自然特征,因為使用new關鍵字創造出來的對象,實際上都是通過function來模擬定義其構造函數的(雖然ES6開始支持class關鍵字了,但是目前還沒有得到浏覽器廣泛支持),那麼如何使用javascript的自然特征來實現單體模式呢?
var Singleton={ attribute1:true, attribute2:10, method1:function(){ }, method2:function(arg){ } }
這裡定義了一個對象Singleton,內部包含若干屬性和方法,將其包含在頁面中,js載入的時候就創建了這個對象,在調用時使用Singleton.method1來調用,它的實例化是隨著頁面載入js解析執行過程中完成的,我們並沒有使用new關鍵字來實例化這個對象,這也是javascript中實現單體模式和傳統面向對象語言一個很大的不同。這種方式更為簡單易於理解。但是這種方式存在若干缺點,一個很明顯的缺點是它並沒有提供命名空間,其他程序員如果在頁面中也定義了一個Singleton變量,那麼很容易改寫和混淆這個單體對象,於是針對這個問題,將其改寫如下:
var mySpace={}; mySpace.Singleton={ attribute1:true, attribute2:10, method1:function(){ }, method2:function(arg){ } }
這裡首先定義了一個mySpace的命名空間,然後將單體對象Singleton掛載在這個對象的下面,這大大減少了和其他程序員沖突以及誤操作的可能,即使其他人在全局作用域中定義一個Singleton變量,也不會污染到這個單體對象,這就實現了前面定義中所說的劃分命名空間並且將一些相關屬性和方法組織在一起的功能。
這個方法依然存在缺點,這個單體對象的所有屬性和方法都是共有的,外部可隨時訪問和修改,於是采用閉包來模擬私有屬性和方法,如下:
mySpace.Singleton=(function(){ var privateAttribute1=false; var privateAttribute1=[1,2,3]; function privateMethod1(){ } function privateMethod2(){ } return { publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){ privateAttribute1=true; privateMethod1(); }, publicMethod2:function(arg){ privateAttribute1=[4,5,6]; privateMethod2(); } } })();
在這裡我們直接給該單體對象賦值了一個匿名自執行的函數,在該函數中使用var和function關鍵字分別來定義其私有屬性和方法,這些在函數外部(單體對象外部)是無法直接訪問的,因為函數一執行完畢,其內部作用域的空間就會被回收,這也就是能夠利用閉包來模擬私有屬性和方法的原因所在。在該函數(閉包)中,同時最終返回一個對象,這個對象中包含一些公有方法和屬性,在外部可以直接調用,同時這些公有方法由於定義在函數內部,所以可以調用其私有屬性和方法,但是外界只能通過返回的公有方法和屬性來完成某些操作,不能夠直接調用Singleton.privateMethod1這些屬性。這就使得該單體對象既隔離了外界去直接訪問其私有屬性和方法,又提供給外界一些共有屬性和方法去完成某些操作。
這種匿名函數自執行所構造的單體模式在很多js庫中被廣泛使用,但是依然存在一個問題,如果我們在載入頁面的時候並不需要用到該對象,而且該對象的創建比較耗費開銷(如需要進行大量計算或需要多次訪問dom樹及其屬性等)時,合理的做法是需要它的時候再去創建它,而不是隨著js的解析執行直接去創建,這種概念被稱之為惰性加載(lazy loading),於是修改以上代碼如下:
mySpace.Singleton=(function(){ var uniqueInstance; function constructor(){ var privateAttribute1=false; var privateAttribute1=[1,2,3]; function privateMethod1(){ } function privateMethod2(){ } return { publicAttribute1:true, publicAttribute2:10, publicMethod1:function(){ privateAttribute1=true; privateMethod1(); }, publicMethod2:function(arg){ privateAttribute1=[4,5,6]; privateMethod2(); } } } return { getInstance:function(){ if(!uniqueInstance){ uniqueInstance=constructor(); } return uniqueInstance; } } })();
這裡首先在匿名函數中定義了一個私有變量uniqueInstance,作為一個判斷單體對象是否被創建出來的句柄,然後將剛才所有對單體對象定義的屬性和方法都放在一個名為constructor的函數中,只有該函數調用了,才會創造出該單體對象,否則不會直接創建它。然後,返回一個對象,其包含一個getInstance方法,該方法是供外部調用的,調用該方法的時候首先判斷該單體對象是否存在,如果存在就直接返回它,否則調用constructor函數構造這個單體對象再返回它。最後如果我們調用該單體對象的某個方法,需要使用mySpace.Singleton.getInstance().publicMethod1(),這裡,只有我們這樣調用的時候才會創建這個單體對象,否則該單體對象是不會被自動創建的,這實際上就實現了按需加載或者惰性加載。