文章初衷
設計模式其實旨在解決語言本身存在的缺陷
目前javaScript一些新的語法特性已經整合了一些設計模式的實現,
大家在寫程式碼的時候,沒必要為了用設計模式而去用設計模式,
那麼我這邊為什麼還寫設計模式的文章呢,
一方面是自己的一個整理,然後記錄出來,結合自己的理解,
一方面就是雖然語言特性本身已經實現這些模式,有了自己的語法,
但是我們何嘗不能去了解一下它是通過什麼樣的思路去實現了
在我看來設計模式更多的是讓我對於思考問題,有了一些更好的思路和想法
文章實現更多的表現為用一些簡單的案例,幫助大家去理解這樣的一種思路,
會存在故意把設計模式的實現往簡單的案例靠攏,
大家在真實專案中不要刻意去用設計模式實現相同的程式碼
設計模式在平時的一些程式碼中都會有所體現,大家也許經常用到,
耐心看文章,也許你會發現自己平時的程式碼就不斷在設計模式中體現
JavaScript設計模式系列
JavaScript設計模式系列,講述大概20-30種設計模式在JavaScript中的運用
後面對應的篇幅會陸續更新,歡迎大家提出建議
這是設計模式系列第三篇,講述單例模式
注意
深入系列文章部分是有先後順序的,按照目錄結構順序閱讀效果最好。
勘誤及提問
如果有疑問或者發現錯誤,可以在相應的 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中,建議採用名稱空間的方式去實現單例,
在名稱空間下面擴充套件對應的屬性還有方法,
以模組化的方式去完成對應的功能模組避免濫用單例模式,因為使用了閉包,過渡使用會導致記憶體洩露
注意
深入系列文章部分是有先後順序的,按照目錄結構順序閱讀效果最好。