轉(zhuǎn)自:http://dickeylth.github.io/2013/10/11/JavaScriptDesignPatterns-SingletonPattern/
最近開始系統(tǒng)學習設計模式檬洞,雖然以前偶爾有接觸森逮,但總感覺不夠系統(tǒng),正好需要做這方面的分享,遂決定來系統(tǒng)學習和記錄一下筛武。
設計模式是程序設計中老生常談的話題了,簡單說就是針對某些可抽象為類似問題的通用的解決方案睛低。雖然方案的思路是死的诀浪,但在不同語言中的實現(xiàn)由于語言間的特性會有些差異,尤其對于像 JavaScript 這樣的動態(tài)語言而言味廊,可能某些設計模式實現(xiàn)起來相比靜態(tài)語言更為靈活蒸甜。
當然,除了提供通用的解決方案余佛,個人感覺更重要的是設計模式的出現(xiàn)提供了各種模塊解耦的思路柠新,為什么需要模塊解耦?因為大多數(shù)時候我們不太可能總是推倒從來辉巡,而往往是在現(xiàn)有的系統(tǒng)基礎之上做進一步的優(yōu)化完善恨憎,系統(tǒng)中模塊之間的耦合度越低,可擴展性就會越強郊楣,從而可以支撐更為復雜的業(yè)務場景和需求憔恳。另外,設計模式也是為了應對駕馭復雜系統(tǒng)的代碼組織架構(gòu)净蚤,熟練掌握之后會對于系統(tǒng)的架構(gòu)有更進一步的認識钥组,從而提升自己在業(yè)務上獨當一面的能力。
其實今瀑,設計模式并不是很遙遠的東西程梦,很可能很多時候自己已經(jīng)在用了而沒有感覺出來,比如 JavaScript 中的全局唯一變量就可以看作是一種單例模式橘荠。更宏觀一點來看屿附,其實設計模式在社會中早已存在,在計算機被創(chuàng)造出來砾医,人類已經(jīng)在應用它了拿撩,比如“烽火戲諸侯”不就是一種觀察者模式(也叫 pub-sub 注冊發(fā)布模式)?所以在這個系列中如蚜,我會盡可能從貼近生活的角度來闡釋每種模式在身邊的例子压恒,從而更易于理解模式的思想影暴。
因此,基于 JavaScript 的設計模式探赫,更多地應該考慮從語言特性型宙、場景和環(huán)境出發(fā),不求形似但求神似伦吠,重要是模式中傳達出的解決思路妆兑,領悟了這一點比死記硬背要有用得多,當然還離不開最重要的熟練應用毛仪。在這個系列中讓我們一起來看一下設計模式與 JavaScript 會碰撞出什么樣的火花搁嗓。
參考數(shù)目:
Learning JavaScript Design Patterns
JavaScript Patterns
一、要解決的問題
單例模式主要目的是確保系統(tǒng)中某個類只存在唯一一個實例箱靴,也就是說對于這個類的重復實例的創(chuàng)建始終只返回同一個實例腺逛。它和工廠模式一樣主要是為了解決對象的創(chuàng)建問題。從前面的描述我們可以看出單例模式的幾大特點:
- 這個類只有一個實例衡怀;
- 該類需要負責實例的初始化工作棍矛;
- 對外需提供這個唯一實例的訪問接口。
生活中有單例模式存在嗎抛杨?有够委,比如大家都知道的12306 是唯一購票網(wǎng)站,所有人要網(wǎng)上訂票都得訪問這個單例怖现。再比如茁帽,法律規(guī)定,每個中國男人都只能有一個合法妻子屈嗤,當然現(xiàn)實之中還有離婚再婚脐雪,單例模式更像是理想狀況下的白頭偕老了。
單例模式帶來的好處恢共?除了減少不必要的重復的實例創(chuàng)建、減少內(nèi)存占用外璧亚,更重要的是避免多個實例的存在造成邏輯上的錯誤讨韭。比如超級馬里奧的游戲中,雖然各種小怪的實例會不斷創(chuàng)建多個癣蟋,但當前的玩家肯定只有一個透硝,如果游戲運行過程中創(chuàng)建出新的馬里奧的實例了,顯然就出 bug 了疯搅。
二濒生、單例模式的實現(xiàn)方法及分析
2.1對象字面量
對于 Java 之類的靜態(tài)語言而言,實現(xiàn)單例模式常見的方法就是將構(gòu)造函數(shù)私有化幔欧,對外提供一個比如名為getInstance方法的靜態(tài)接口來訪問初始化好的私有靜態(tài)的類自身實例罪治。但對于 JavaScript 這樣的動態(tài)語言而言丽声,單例模式的實現(xiàn)其實可以很靈活,因為 JavaScript 語言中并不存在嚴格意義上的類的概念觉义,只有對象雁社。每次創(chuàng)建出的新對象都和其他的對象在邏輯上不相等,即使它們是具有完全相同的成員屬性的同構(gòu)造器創(chuàng)造出的對象晒骇。所以霉撵,在 JavaScript 中,最常見的單例模式莫過于對象字面量(object literal)了:
var x = {
attr: 'value'
};
var y = {
attr: 'value'
};
x == y; // false
x === y; // false
可見洪囤,對象字面量就是一種最簡單最常見的單例模式了徒坡。在全局的其他地方要獲得這個單例的對象,其實就是獲得這個唯一的全局變量就可以保證訪問的是同一實例了瘤缩。
上面的對象字面量僅僅是一個簡單的鍵值對喇完,但很多時候?qū)ο罂赡苓€涉及到初始化的工作,可能需要實現(xiàn)按需加載(懶加載)款咖,對象中還會存在內(nèi)部私有成員何暮,對外需以門面模式(Facade)
提供可訪問的接口。所以我們還可以把這個簡單的對象字面量再擴充一下:
var SuperMario = (function(){
var instance = null;
// 初始化函數(shù)
function init(){
var gener = 'male',
age = 12,
height = 120;
// 門面模式返回成員屬性
return {
name: 'Mario',
getAge: function(){
return age;
},
getHeight: function(){
return height;
},
jump: function(){
console.log("I'm jumping!");
},
run: function(){
console.log("I'm running!");
}
};
}
return {
getInstance: function(){
if(!instance){
instance = init();
}
return instance;
}
};
})();
console.log(SuperMario.getInstance());
在 Chrome 控制臺下運行可以得到如下結(jié)果:
讓我們來簡單分析一下這段代碼铐殃。首先依然是給對象賦值海洼,但是采用的是即時函數(shù)的方式,從而創(chuàng)建出一個閉包富腊,里面存放著 SuperMario
的真身——instance
坏逢,在結(jié)尾時暴露一個getInstance
方法向外提供該實例的引用,有點像靜態(tài)語言中的單例模式了吧赘被?
在這個閉包之內(nèi)是整,創(chuàng)建了一個內(nèi)部私有的init
初始化函數(shù),完成 SuperMario
對象的初始化工作民假。注意到這里再一次使用了閉包浮入,將age
、height
這些私有成員的值保護起來羊异,對外只提供getter
訪問器事秀,不允許外部代碼對其修改。除此之外野舶,還向外提供了可公開的run
易迹、jump
方法。
為了實現(xiàn)懶加載平道,ini
t初始函數(shù)并不是自動執(zhí)行的睹欲,而是調(diào)用getInstance
方法時檢查到當前instance
還沒有被初始化過時才會去執(zhí)行init
,而在下次再次getInstance
時就直接返回之前已初始化好的實例了,這樣就不至于給頁面的初始化工作帶來太大的負擔窘疮,而是需要使用的時候按需完成初始化袋哼。
2.2 使用new
創(chuàng)建對象
雖然 JavaScript
中沒有類,但是卻也具有new
這個關(guān)鍵字來利用構(gòu)造函數(shù)創(chuàng)建對象考余。對于這種形式創(chuàng)建的對象先嬉,要實現(xiàn)單例模式的思想,就需要保證每次new
出來的對象都是對同一對象的指針楚堤。也就是說預期應該像下面的代碼這樣:
var x = new SuperMario();
var y = new SuperMario();
x == y; // true
因此需要保證 x
和y
指向的是同一個SuperMario
構(gòu)造函數(shù)構(gòu)造出的對象疫蔓,即第二次調(diào)用SuperMario
構(gòu)造函數(shù)返回的是第一次調(diào)用時構(gòu)造出的實例的引用,同樣以后每次調(diào)用該構(gòu)造函數(shù)返回的應該都是這同一實例的引用身冬。那么實現(xiàn)上主要就是要解決這個實例的存放位置問題衅胀,有幾種選擇方案:
- 使用全局變量來存儲。當然這種方案一般都不值得推薦酥筝;
- 緩存到SuperMario構(gòu)造函數(shù)的靜態(tài)屬性中滚躯,實現(xiàn)起來也比較簡潔,但缺點是不能避免該靜態(tài)屬性被外部代碼
修改嘿歌,畢竟 JavaScript 不像靜態(tài)語言能做到對靜態(tài)屬性的寫保護掸掏; - 借助閉包實現(xiàn)。這樣可以確保實例的內(nèi)部私有性宙帝,缺點是額外的開銷丧凤,這是引入閉包必然會帶來的弊端。
下面分別看看后兩種方案的具體實現(xiàn)步脓。
2.2.1 靜態(tài)屬性中的實例
采用靜態(tài)屬性的方式代碼比較簡單易懂愿待,基本的結(jié)構(gòu)類似這樣:
// 定義
function SuperMario(){
// 判斷當前靜態(tài)屬性是否已存在
if(typeof SuperMario.instance === "object"){
return SuperMario.instance;
}
// 定義屬性值
this.name = "Mario";
this.age = 12;
this.gener = "male";
// 緩存到靜態(tài)屬性中
SuperMario.instance = this;
// 可要可不要,默認隱式返回 this
return this;
}
// 執(zhí)行
var x = new SuperMario();
var y = new SuperMario();
x == y; // true
看上去很簡單對吧靴患?不過問題來了:
如果在執(zhí)行部分添加一行代碼:
// 執(zhí)行
var x = new SuperMario();
SuperMario.instance = null;
var y = new SuperMario();
x == y; // ?
console.log(y); // ?
你肯定已經(jīng)猜到了此時 x == y
結(jié)果是false
仍侥,而對于下一行呢?console.log(y)
將輸出什么呢鸳君?
更進一步地农渊,如果我們在SuperMario
的構(gòu)造函數(shù)中再加一行:
// 定義
function SuperMario(){
this.attr = 'value';
// 判斷當前靜態(tài)屬性是否已存在
if(typeof SuperMario.instance === "object"){
return SuperMario.instance;
}
...
}
此時console.log(y)
又會返回什么呢?
其實這里涉及到的是一個構(gòu)造函數(shù)返回值的問題或颊,大多數(shù)情況下我們都不會在構(gòu)造函數(shù)中顯式返回值腿时,因為默認的 this 會自動隱式返回。說到這里饭宾,你可能需要先深入了解下當以new操作符調(diào)用構(gòu)造函數(shù)時到底發(fā)生了什么?
當以new操作符調(diào)用構(gòu)造函數(shù)時格了,函數(shù)內(nèi)部將會發(fā)生以下情況:
- 創(chuàng)建一個空對象并且
this
變量引用了該對象看铆,同時還繼承了該函數(shù)的原型; - 屬性和方法被加入到
this
引用的對象中盛末; - 新創(chuàng)建的對象由
this
所引用弹惦,并且最后隱式地返回this
(如果沒有顯式地返回其他對象)
JavaScript PatternsStoyan Stefanov(中文版 P45)
那么在構(gòu)造函數(shù)中定義了 return
時否淤,以new
調(diào)用的結(jié)果是怎樣的呢?
在 stackoverflow
上也有類似的問題:What values can a constructor return to avoid returning this?棠隐,第一個回答的引用石抡,也就是ECMA-262 中定義了返回策略。
我們也可以把結(jié)論簡單記為兩條:
當return
一個引用對象(數(shù)組助泽、函數(shù)啰扛、對象等)時,直接覆蓋內(nèi)部的隱式this
對象嗡贺,返回值就是該引用對象隐解;當return
5 種基本類型(undefined
、null
诫睬、Boolean
煞茫、Number
、String
)之一時(無return
時其實就是返回undefined
)摄凡,返回內(nèi)部隱式this
對象续徽。
還需要注意一點,基本類型可以以包裝器包裝成對象亲澡,所以:
function SuperMario(){
...
return new String('mario');
return 'mario';
}
兩者的返回值就不一樣了钦扭。
現(xiàn)在你應該可以得出上面的問題的答案了吧?
2.2.2 閉包中的實例
采用閉包的方式一般將初始化后的實例用閉包保護起來谷扣,而后重寫構(gòu)造函數(shù)直接返回該實例土全,于是我們可以簡單得到以下代碼:
function Person(){
var instance = this;
this.attr = "Attribute";
Person = function(){
return instance;
};
}
var p1 = new Person();
var p2 = new Person();
但這樣會有什么潛在的問題呢?我們來稍作一點變化:
function Person(){
var instance = this;
this.attr = "Attribute";
Person = function(){
console.log(this);
return instance;
};
}
Person.prototype.job = 'FE';
var p1 = new Person();
Person.prototype.city = 'Beijing';
var p2 = new Person();
console.log(p1);
console.log(p2);
console.log(p1.constructor === Person);
//console.log(Person);
出現(xiàn)什么問題了会涎?之后給Person
類添加的prototype
屬性被丟失了裹匙,這卻是為何?因為我們重寫了構(gòu)造函數(shù)末秃,結(jié)果月亮還是那個月亮概页,Person
卻不再是那個Person
了。在第二次new Person()
時我們可以打印出此時的this
练慕,可以看到它是繼承了后面掛載的city
原型屬性的惰匙,但因為原來的Person
已經(jīng)被覆蓋了,所以原來的job
屬性就找不到了铃将。而后我們return instance
的執(zhí)行项鬼,根據(jù)上文中的結(jié)論,就會直接覆蓋構(gòu)造函數(shù)中的隱式this
劲阎,結(jié)果就丟掉了后面增加的原型屬性city
了绘盟。
有沒有改進的方法呢?經(jīng)過了上面的分析,我們就可以知道龄毡,要解決這個問題吠卷,關(guān)鍵是除了重寫構(gòu)造函數(shù)之外,還需要修復繼承鏈和構(gòu)造函數(shù)沦零,于是可以得到下面的代碼:
function Person(){
var instance;
Person = function Person(){
return instance;
};
Person.prototype = this; // this
instance = new Person();
instance.constructor = Person;
instance.attr = "Attribute";
return instance;
}
Person.prototype.job = 'FE';
var p1 = new Person();
Person.prototype.city = 'Beijing';
var p2 = new Person();
console.log(p1);
console.log(p2);
console.log(p1.constructor === Person);
其實這個時候重寫后的Person
類實質(zhì)上變成了之前老的Person
類的子類了祭隔,它們之間就是通過這句Person.prototype = this
;聯(lián)系起來的。我們也可以在控制臺看看Person
展開后的樣子來認識一下這個新的Person
路操。
最后疾渴,留一個小問題:
...
// 重寫該構(gòu)造函數(shù)
Person = function Person(){
return instance;
};
這里的·function Person中的Person是干嘛用的呢?
三寻拂、在開源框架和類庫中的應用
單例模式在開源框架中應用其實很廣泛程奠,細數(shù)一下我們熟悉的前端開源框架和類庫:jQuery
、YUI
祭钉、underscore
瞄沙、KISSY
等,大多都有一個全局變量慌核,比如 jQuery
中的jQuery
(或$
)距境、YUI
中的YUI
、underscore
中的_
垮卓、KISSY
中的KISSY
垫桂,這就是一種單例。讓我們來看看 jQuery
:
(function( window, undefined ) {
var jQuery = (function() {
// 構(gòu)建 jQuery 對象
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery );
}
// jQuery 對象原型
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function( selector, context, rootjQuery ) {
// selector 有以下 7 種分支情況:
// DOM 元素
// body(優(yōu)化)
// 字符串:HTML 標簽粟按、HTML 字符串诬滩、#id、選擇器表達式
// 函數(shù)(作為 ready 回調(diào)函數(shù))
// 最后返回偽數(shù)組
}
};
// 猜猜這句是干什么呢灭将?
jQuery.fn.init.prototype = jQuery.fn;
// 合并內(nèi)容到第一個參數(shù)中疼鸟,后續(xù)大部分功能都通過該函數(shù)擴展
// 通過 jQuery.fn.extend 擴展的函數(shù),大部分都會調(diào)用通過 jQuery.extend 擴展的同名函數(shù)
jQuery.extend = jQuery.fn.extend = function() {};
// 在 jQuery 上擴展靜態(tài)方法
jQuery.extend({
// ready bindReady
// isPlainObject isEmptyObject
// parseJSON parseXML
// globalEval
// each makeArray inArray merge grep map
// proxy
// access
// uaMatch
// sub
// browser
});
// 到這里庙曙,jQuery 對象構(gòu)造完成空镜,后邊的代碼都是對 jQuery 或 jQuery 對象的擴展
return jQuery;
})();
window.jQuery = window.$ = jQuery;
})(window);
通過上面的 jQuery 代碼的總體結(jié)構(gòu),可見它同樣是采用的是類似上面對象字面量形式創(chuàng)建全局的 jQuery 對象捌朴,在其中又重定義了構(gòu)造函數(shù)吴攒,完成初始化工作,最后返回新的 jQuery 對象砂蔽。
四洼怔、總結(jié)
通過上面的源碼簡析,個人覺得左驾,在 JavaScript 中應用單例模式采用對象字面量的方式更易讀易懂茴厉,應用也更為廣泛泽台,而從理論角度采用閉包模擬類似靜態(tài)語言的單例的概念的方式,雖然也可以實現(xiàn)矾缓,但失掉了 JavaScript 作為一門動態(tài)語言的優(yōu)勢,而且代碼相比之下可維護性差了些稻爬。當然采用對象字面量方式需要與使用者達成約定嗜闻,即直接調(diào)用該實例而非通過構(gòu)造函數(shù)來獲得實例,這種調(diào)用方式也很自然桅锄。