JavaScript設計模式系列三之單例模式(附案例原始碼)

小錢錢阿聖發表於2017-09-22

文章初衷

設計模式其實旨在解決語言本身存在的缺陷

目前javaScript一些新的語法特性已經整合了一些設計模式的實現,

大家在寫程式碼的時候,沒必要為了用設計模式而去用設計模式,

那麼我這邊為什麼還寫設計模式的文章呢,

一方面是自己的一個整理,然後記錄出來,結合自己的理解,

一方面就是雖然語言特性本身已經實現這些模式,有了自己的語法,

但是我們何嘗不能去了解一下它是通過什麼樣的思路去實現了

在我看來設計模式更多的是讓我對於思考問題,有了一些更好的思路和想法

文章實現更多的表現為用一些簡單的案例,幫助大家去理解這樣的一種思路,

會存在故意把設計模式的實現往簡單的案例靠攏,

大家在真實專案中不要刻意去用設計模式實現相同的程式碼

設計模式在平時的一些程式碼中都會有所體現,大家也許經常用到,

耐心看文章,也許你會發現自己平時的程式碼就不斷在設計模式中體現

JavaScript設計模式系列

JavaScript設計模式系列,講述大概20-30種設計模式在JavaScript中的運用

後面對應的篇幅會陸續更新,歡迎大家提出建議

這是設計模式系列第三篇,講述單例模式

上篇文章講述了建造者設計模式,有興趣可以檢視

注意

JavaScript設計模式系列github地址

深入系列文章部分是有先後順序的,按照目錄結構順序閱讀效果最好。

勘誤及提問

如果有疑問或者發現錯誤,可以在相應的 issues 進行提問或勘誤。

單例模式

概念:

單例模式主要目的是確保系統中某個類只存在唯一一個例項,

也就是說對於這個類的重複例項的建立始終只返回同一個例項

在JavaScript裡,單例可以作為一個名稱空間提供者,

從全域性名稱空間裡提供一個唯一的訪問點來訪問該物件

需要解決的問題

1.就拿我們常玩的超級瑪麗遊戲來說,遊戲中小怪會不斷出現,但是超級瑪麗只有一個,如果遊戲中出現多個瑪麗例項的話,那就會出現問題,這時候就會用到我們所說的單例模式

2.再比如說我們頁面視窗的彈窗,彈窗大部分情況下我們只需要一個,如果同時出現多個錯誤彈窗的話,網頁就會顯示的很奇怪,這時候彈窗的建立也會用到我們所說的單例模式

案例體現

那我們下面就來看一下單例模式在程式碼中的體現

對於 Java 之類的靜態語言而言,實現單例模式常見的方法就是

將建構函式私有化,對外提供一個比如名為getInstance方法

靜態介面來訪問初始化好的私有靜態的類自身例項。

但對於 JavaScript這樣的動態語言而言,

單例模式的實現其實可以很靈活,

因為 JavaScript 語言中並不存在嚴格意義上的類的概念,只有物件。

每次建立出的新物件都和其他的物件在邏輯上不相等,

即使它們是具有完全相同的成員屬性的同構造器創造出的物件。

所以,在 JavaScript 中,最常見的單例模式莫過於全域性變數了

這邊就拿上面提到的超級瑪麗遊戲舉一個簡單的?

// 遊戲人物
var Player = {
    hp: 100,
    mp: 200
}複製程式碼

可見,全域性變數就是一種最簡單最常見的單例模式了。在全域性的其他地方要獲得這個單例的物件,其實就是獲得這個唯一的全域性變數,就可以保證訪問的是同一例項

之前提過在JavaScript裡,單例可以作為一個名稱空間提供者,
從全域性名稱空間裡提供一個唯一的訪問點來訪問該物件

那麼我們下面來看一下具體實現

名稱空間概念:

名稱空間就是所謂的namespace,它解決這麼一類問題:

為了讓程式碼更易懂,人們常常用單詞或者拼音定義變數或者方法,

但是因為可用的單詞或者拼音有限,不同人定義的變數可能重複,

此時就需要用名稱空間來約束每個人定義的變數來解決這類問題.

看一段程式碼

/**
 * 名稱空間
 */

var Main = {
    run: function() {
        console.log('run');
    },
    jump: function() {
        console.log('jump');
    }
}
Main.run();複製程式碼

這邊定義了一個Main的名稱空間,那麼所有的屬性還有方法都會

放到Main這個名稱空間下面,自然就可以保證變數名的唯一性.

jQuery這邊也是用的同樣的方式,定義了一個jQuery的名稱空間,

所以的jQuery方法還有屬性都放到這個名稱空間,

這樣就不會跟別的庫的程式碼衝突


上面的名稱空間案例僅僅是一個簡單的物件,

但很多時候物件可能還涉及到初始化的工作,

可能需要實現按需載入(懶載入),物件中還會存在內部私有成員,

對外需以門面模式(Facade)提供可訪問的介面。

所以我們還可以把這個再擴充一下:

之前有提過單例模式在Java中的實現方式,複習一下

對於 Java 之類的靜態語言而言,實現單例模式常見的方法就是將

建構函式私有化,對外提供一個比如名為getInstance方法

靜態介面來訪問初始化好的私有靜態的類自身例項。

我們這邊嘗試一下Java中的單例模式在javascript中的實現,看?

/ 遊戲人物
var Player = (function() {
    // 例項
    var instance = null;
    // 私有變數
    var hp = 100; // 生命值
    var mp = 200; // 魔法值
    // 初始化函式
    function init() {
        return {
            // 獲取生命值
            getHp: function() {
                return hp;
            },
            // 獲取魔法值
            getMp: function() {
                return mp;
            },
            // 行走方法
            run: function() {
                console.log('running');
            }
        }
    }
    return {
        // 初始化一個遊戲英雄
        init: function() {
            // 未存在,則初始化
            if (!instance) {
                instance = init();
            }
            return instance;
        }
    }
})();

// 建立一個遊戲英雄
var person1 = Player.init();
console.log(person1.getHp());
console.log(person1.getMp());
// 死亡復活
var person2 = Player.init();
// 建立的還是之前的物件
console.log(person1 === person2); // true複製程式碼

這邊建立一個Player類,利用立刻執行函式,

然後建立一個空的例項變數instance用來記錄是否已經例項化,

建立2個私有變數hp,mp,建立一個初始化函式init,

利用閉包的方式,暴露對外訪問的介面,

細心的人可以看到程式碼裡面輸出的person1 完全等於person2

因為提供了公有的靜態方法init,負責檢驗例項的存在性並例項化自己,

然後儲存在例項物件instance中,以確保只有一個例項被建立

init: function() {
            // 未存在,則初始化
            if (!instance) {
                instance = init();
            }
            return instance;
        }複製程式碼

在jQuery中的應用

(function( window, undefined ) {

    var jQuery = (function() {
       // 構建 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(優化)
              // 字串:HTML 標籤、HTML 字串、#id、選擇器表示式
              // 函式(作為 ready 回撥函式)
              // 最後返回偽陣列
           }
       };

       // 猜猜這句是幹什麼呢?
       jQuery.fn.init.prototype = jQuery.fn;

       // 合併內容到第一個引數中,後續大部分功能都通過該函式擴充套件
       // 通過 jQuery.fn.extend 擴充套件的函式,大部分都會呼叫通過 jQuery.extend 擴充套件的同名函式
       jQuery.extend = jQuery.fn.extend = function() {};

       // 在 jQuery 上擴充套件靜態方法
       jQuery.extend({
           // ready bindReady
           // isPlainObject isEmptyObject
           // parseJSON parseXML
           // globalEval
           // each makeArray inArray merge grep map
           // proxy
           // access
           // uaMatch
           // sub
           // browser
       });

        // 到這裡,jQuery 物件構造完成,後邊的程式碼都是對 jQuery 或 jQuery 物件的擴充套件
       return jQuery;

    })();

    window.jQuery = window.$ = jQuery;
})(window);複製程式碼

jQuery這邊也採用了上面的方式建立了jQuery名稱空間,
然後對應的屬性還有方法都在這個名稱空間下面擴充套件,
jQuery這邊也把建構函式私有化,在呼叫的時候完成例項化,
不同點是jQuery每次都返回新的物件

單例模式的優點

  • 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件,單例模式無疑可以提高系統的效能。

  • 名稱空間的管理方式,可以有效避免變數的重複,儘量避免使用全域性變數

單例模式的缺點

  • 單例模式實現使用了閉包,過多使用閉包會導致記憶體洩露

  • 單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的建立和產品的本身的功能融合到一起。

單例模式適用情況

  • 系統只需要一個例項物件,或者需要考慮資源消耗太大而只允許建立一個物件。

  • 在javascript中,把功能封裝起來,形成一個小型程式碼庫,就可以使用名稱空間的方式去實現,按照模組化劃分

單例模式總結

  • 在javascript中,建議採用名稱空間的方式去實現單例,
    在名稱空間下面擴充套件對應的屬性還有方法,
    以模組化的方式去完成對應的功能模組

  • 避免濫用單例模式,因為使用了閉包,過渡使用會導致記憶體洩露

注意

JavaScript設計模式系列github地址

深入系列文章部分是有先後順序的,按照目錄結構順序閱讀效果最好。

關於設計模式,更多的是結合我自己的一些理解,把他總結出來分享給大家,如果大家發現有什麼不正確的地方,希望大家一定得提出來,避免我這邊誤人子弟,感謝大家!!!

相關文章