JavaScript中的設計模式(1)——單例模式

轉(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)建問題。從前面的描述我們可以看出單例模式的幾大特點:

  1. 這個類只有一個實例衡怀;
  2. 該類需要負責實例的初始化工作棍矛;
  3. 對外需提供這個唯一實例的訪問接口。

生活中有單例模式存在嗎抛杨?有够委,比如大家都知道的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é)果:


result

讓我們來簡單分析一下這段代碼铐殃。首先依然是給對象賦值海洼,但是采用的是即時函數(shù)的方式,從而創(chuàng)建出一個閉包富腊,里面存放著 SuperMario 的真身——instance坏逢,在結(jié)尾時暴露一個getInstance方法向外提供該實例的引用,有點像靜態(tài)語言中的單例模式了吧赘被?
在這個閉包之內(nèi)是整,創(chuàng)建了一個內(nèi)部私有的init初始化函數(shù),完成 SuperMario 對象的初始化工作民假。注意到這里再一次使用了閉包浮入,將ageheight這些私有成員的值保護起來羊异,對外只提供getter訪問器事秀,不允許外部代碼對其修改。除此之外野舶,還向外提供了可公開的run易迹、jump方法。
為了實現(xiàn)懶加載平道,init初始函數(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

因此需要保證 xy 指向的是同一個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ā)生以下情況:

  1. 創(chuàng)建一個空對象并且 this變量引用了該對象看铆,同時還繼承了該函數(shù)的原型;
  2. 屬性和方法被加入到this引用的對象中盛末;
  3. 新創(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對象嗡贺,返回值就是該引用對象隐解;當return5 種基本類型(undefinednull诫睬、Boolean煞茫、NumberString)之一時(無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ù)一下我們熟悉的前端開源框架和類庫:jQueryYUI祭钉、underscore瞄沙、KISSY 等,大多都有一個全局變量慌核,比如 jQuery 中的jQuery(或$)距境、YUI 中的YUIunderscore 中的_垮卓、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)用方式也很自然桅锄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末琉雳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子友瘤,更是在濱河造成了極大的恐慌翠肘,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辫秧,死亡現(xiàn)場離奇詭異束倍,居然都是意外死亡,警方通過查閱死者的電腦和手機盟戏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門绪妹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柿究,你說我怎么就攤上這事邮旷。” “怎么了蝇摸?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵婶肩,是天一觀的道長。 經(jīng)常有香客問我貌夕,道長律歼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任蜂嗽,我火速辦了婚禮苗膝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘植旧。我一直安慰自己辱揭,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布病附。 她就那樣靜靜地躺著问窃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪完沪。 梳的紋絲不亂的頭發(fā)上域庇,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天嵌戈,我揣著相機與錄音,去河邊找鬼听皿。 笑死熟呛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的尉姨。 我是一名探鬼主播庵朝,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼又厉!你這毒婦竟也來了九府?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤覆致,失蹤者是張志新(化名)和其女友劉穎侄旬,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煌妈,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡儡羔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了声旺。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笔链。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖腮猖,靈堂內(nèi)的尸體忽然破棺而出鉴扫,到底是詐尸還是另有隱情,我是刑警寧澤澈缺,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布坪创,位于F島的核電站,受9級特大地震影響姐赡,放射性物質(zhì)發(fā)生泄漏莱预。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一项滑、第九天 我趴在偏房一處隱蔽的房頂上張望依沮。 院中可真熱鬧,春花似錦枪狂、人聲如沸危喉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辜限。三九已至,卻和暖如春严蓖,著一層夾襖步出監(jiān)牢的瞬間薄嫡,已是汗流浹背氧急。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毫深,地道東北人吩坝。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像哑蔫,于是被迫代替她去往敵國和親钾恢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容