JavaScript設計模式(一)單例模式、組合模式和外觀模式
單例模式是指在您要確保始終只建立一個物件例項時使用的設計模式。 在面向物件的經典程式語言中,建立單例模式背後的概念多少有點讓人難以理解,因為其中包含一個同時具有靜態及非靜態屬性和方法的類。 但本文主要討論 JavaScript,因此,由於 JavaScript 是一種不包含真正類的動態語言,因此 JavaScript 版本的單例模式極其簡單。
為什麼需要採用單例模式?
在我開始介紹實施細節之前,我需要探討一下為什麼單例模式對於應用程式非常有用。 它能夠確保您只有一個物件例項能夠實際派上用場。 在伺服器端語言中,您可能會使用單例處理資料庫連線,這是由於為一個請求建立多個數據庫連線純粹是一種資源浪費。 同樣,在前端 JavaScript 中,您可能會希望將負責處理所有 AJAX 請求的某個物件設定為單例。 規則非常簡單: 如果每次建立新例項時,例項的功能均完全相同,那麼將其設定為單例。
但是,這並不是採用單例的唯一原因。 至少在 JavaScript 中,單例可讓您保證名稱空間物件和函式井然有序,防止它們與全域性名稱空間混淆,您可能明白,這是一種可怕的想法,特別是在使用第三方程式碼的情況下。 使用名稱空間單例模式也被稱為模組設計模式。
展示單例模式
要建立單例,您只需建立一個物件文字。
var Singleton = {
prop: 1,
another_prop: 'value',
method: function() {…},
another_method: function() {…}
};
您還可以建立具有私有屬性和方法的單例,但由於其涉及使用封閉函式和自呼叫匿名函式,因而稍微有些難以理解。 函式內部聲明瞭一些區域性函式和/或變數。 然後,建立並返回一個物件文字,其中包含一些引用您在更大的函式範圍內宣告的變數和函式的方法。 緊隨函式宣告放置 () 即可立即執行外部函式,並將所得的物件文字分配給變數。 如果這些介紹讓您感到困惑,那麼請看下面的程式碼,隨後我將會做出進一步的說明。
var Singleton = (function() { var private_property = 0, private_method = function () { console.log('This is private'); } return { prop: 1, another_prop: 'value', method: function() { private_method(); return private_property; }, another_method: function() {…} } }());
關鍵在於,當通過某個變數所在函式前方的 Var 宣告該變數時,該變數只能在函式內部通過該函式內宣告的各函式(例如物件文字內的函式)進行訪問。 return 語句可幫助我們回到在外部函式自行執行後分配給單例的物件文字。
單例模式名稱空間
在 JavaScript 中,名稱空間化通過將物件作為另一物件的屬性新增來完成,因此深度為一個或多個圖層。 這對於將程式碼組合成邏輯片段非常有用。 雖然我認為 YUI JavaScript 庫在一定程度上名稱空間層次過多,但總體而言仍可算作將巢狀名稱空間限制在只有幾個或更少圖層的最佳實踐。 以下程式碼是一個名稱空間示例。
var Namespace = {
Util: {
util_method1: function() {…},
util_method2: function() {…}
},
Ajax: {
ajax_method: function() {…}
},
some_method: function() {…}
};
// Here's what it looks like when it's used
Namespace.Util.util_method1();
Namespace.Ajax.ajax_method();
Namespace.some_method();
如前所述,使用名稱空間保證全域性變數數量最低。 您甚至可以將整個應用程式連線到單一物件名稱空間命名的應用程式(如果您有這項特權的話)。 如果您想繼續瞭解有關單例設計模式及其名稱空間適用性的更多資訊,請繼續學習,在我的個人部落格上檢視文章"JavaScript 設計模式:單例模式"。
組合模式
如果您在通讀單例模式一節後,認為“嗨,這很簡單”,那麼不要著急,我還有一些更加複雜的模式要討論,其中一個就是組合模式。 組合,顧名思義是指用包含多個部件的物件建立單一實體。 這個單一實體將用作所有這些部件的訪問點,雖然這大大簡化了操作,但也可能具有相當的欺騙性,因為沒有哪種隱性方式明確表明該組合包含多少部件。
組合結構
我們最好使用例證解說組合。 在圖 1 中,您可以看到兩種不同型別的物件: 容器和庫是組合,影象是葉片。 組合可承載子項,但一般不會實施更多行為。 葉片包含絕大多數行為,但不能承載子項,至少在傳統的組合示例中不可以。
另一個示例,我本人百分百確定您之前見到過組合模式,但從未真正進行深入思考。 計算機檔案結構是組合模式的一個例項。 如果您刪除某個資料夾,也將刪除該資料夾的所有內容,是嗎? 這實質上就是組合模式執行原理。 您可以呼叫結構樹上較高層次的組合物件,訊息將沿這一層次結構向下傳輸。
組合編碼示例
此示例建立圖片庫,將其作為組合模式示例。 只有三個層次: 專輯、庫和影象。 專輯和庫將作為組合,影象是葉片,如圖 1 所示。這是一種比組合本身需求更加明確的結構,但對於本示例而言,將這些層次僅限制為組合或葉片很有意義。 標準組合不會限制哪些結構層次可以具有葉片,也不會限制葉片數量。
要開始操作,應首先建立用於專輯和庫的 GalleryComposite“類”。 請注意,我正在使用 jQuery 執行 DOM 操作以簡化過程。
var GalleryComposite = function (heading, id) {
this.children = [];
this.element = $('<div id="' + id + '" class="composite-gallery"></div>')
.append('<h2>' + heading + '</h2>');
}
GalleryComposite.prototype = {
add: function (child) {
this.children.push(child);
this.element.append(child.getElement());
},
remove: function (child) {
for (var node, i = 0; node = this.getChild(i); i++) {
if (node == child) {
this.children.splice(i, 1);
this.element.detach(child.getElement());
return true;
}
if (node.remove(child)) {
return true;
}
}
return false;
},
getChild: function (i) {
return this.children[i];
},
hide: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.hide();
}
this.element.hide(0);
},
show: function () {
for (var node, i = 0; node = this.getChild(i); i++) {
node.show();
}
this.element.show(0);
},
getElement: function () {
return this.element;
}
}
這個位置有點棘手,能否允許我再更多的解釋一下? 我們同時使用 add
, remove
, 和getChild
方法構建這一組合。 本示例不會實際使用 remove
和 getChild
,但它們對於建立動態組合非常有用。 hide
, show
,
和getElement
方法則用來操縱 DOM。 該組合旨在作為庫的 表示在頁面上向用戶展示。 該組合可通過 hide
和 show
控制這些庫元素。 如果在專輯上呼叫 hide
,則整個專輯將消失,或者您也可以只在單一影象上呼叫它,這樣只有該影象會消失。
現在,建立一個 GalleryImage
類。 請注意,它使用的方法與 GalleryComposite
完全相同。 換句話說,它們實現同一介面,不同的是該影象是葉片,因此不會實際對子項相關方法執行任何操作,就像不具有任何子項一樣。 必須使用同一介面執行該組合,因為組合元素不知道自身新增的是另一個組合元素還是葉片,因此如果嘗試在其子項上呼叫這些方法,則需要執行完全正常,沒有任何錯誤。
var GalleryImage = function (src, id) {
this.children = [];
this.element = $('<img />')
.attr('id', id)
.attr('src', src);
}
GalleryImage.prototype = {
// Due to this being a leaf, it doesn't use these methods,
// but must implement them to count as implementing the
// Composite interface
add: function () { },
remove: function () { },
getChild: function () { },
hide: function () {
this.element.hide(0);
},
show: function () {
this.element.show(0);
},
getElement: function () {
return this.element;
}
}
鑑於您已經構建了物件原型,您現已能夠進行使用。 從下面您可以看到實際構建影象庫的程式碼。
var container = new GalleryComposite('', 'allgalleries');
var gallery1 = new GalleryComposite('Gallery 1', 'gallery1');
var gallery2 = new GalleryComposite('Gallery 2', 'gallery2');
var image1 = new GalleryImage('image1.jpg', 'img1');
var image2 = new GalleryImage('image2.jpg', 'img2');
var image3 = new GalleryImage('image3.jpg', 'img3');
var image4 = new GalleryImage('image4.jpg', 'img4');
gallery1.add(image1);
gallery1.add(image2);
gallery2.add(image3);
gallery2.add(image4);
container.add(gallery1);
container.add(gallery2);
// Make sure to add the top container to the body,
// otherwise it'll never show up.
container.getElement().appendTo('body');
container.show();
這就是該組合的全部程式碼! 如果要檢視該庫的現場演示,您可以訪問我部落格上的演示頁。 您還可以閱讀我部落格上的 "JavaScript 設計模式:組合模式"一文更加深入地瞭解這一模式的更多資訊。
外觀模式
外觀模式是本文介紹的最後一種設計模式,它僅僅是一種簡化複雜介面的函式或另一段程式碼。 其實,該模式相當普遍,這時有人可能會說,大多數函式實際上都是為了實現這一目的。 外觀模式的目標在於將大型邏輯片段簡化為一個簡單的函式呼叫操作。
外觀模式示例
您或許一直在使用外觀模式,卻並不認為自己使用了任何設計模式。 您使用的幾乎任何程式語言的影象庫均在一定程度上使用外觀模式,因為它們的普遍用途在於使複雜的事務變得簡單。
讓我們來看一個 jQuery 示例,jQuery 使用一個函式 (jquery()
) 執行多項操作;它可以查詢 DOM、建立元素,也可以只是用來將 DOM 元素轉換為 jQuery 物件。 如果您只是想了解 DOM 查詢,簡單看一下用於建立該功能的程式碼行數量,那麼您可能會對自己說,“我很慶幸自己不必寫這些程式碼”,因為程式碼實在太長太複雜。 為方便您使用,他們已成功使用外觀模式將數百行復雜程式碼轉換為一個簡單的函式呼叫。
結束語
外觀模式很容易理解,但如果您有興趣,可以閱讀我的個人部落格 JavaScript 設計模式:外觀模式一文了解更多資訊。
下一步學習方向
在 JavaScript 設計模式系列文章的這一部分中,我介紹了單例模式、組合模式和外觀模式,隨後在此係列文章的第二部分中,我將向您介紹另外 3 種設計模式,其中包含的一些概念較此處涉及的概念更加複雜。 如果您沒有足夠的耐心,我已經在自己的個人部落格上撰寫了一個有關 JavaScript 設計模式的系列文章,其中還包括幾種我並不打算在此係列文章中介紹的設計模式。 您可以在我的部落格上查詢該系列的所有文章。