JavaScript 設計模式綜合指南
已發表: 2022-03-11作為一名優秀的 JavaScript 開發人員,您努力編寫乾淨、健康和可維護的代碼。 您解決的有趣挑戰雖然獨特,但不一定需要獨特的解決方案。 您可能已經發現自己編寫的代碼看起來與您之前處理過的完全不同的問題的解決方案相似。 您可能不知道,但您使用過 JavaScript設計模式。 設計模式是軟件設計中常見問題的可重用解決方案。
在任何語言的生命週期中,許多此類可重用的解決方案都是由該語言社區的大量開發人員製作和測試的。 正是由於許多開發人員的這種綜合經驗,這些解決方案非常有用,因為它們可以幫助我們以優化的方式編寫代碼,同時解決手頭的問題。
我們從設計模式中獲得的主要好處如下:
- 它們是經過驗證的解決方案:因為許多開發人員經常使用設計模式,所以您可以確定它們是有效的。 不僅如此,您還可以確定它們已被多次修改,並且可能已經實施了優化。
- 它們易於重用:設計模式記錄了可重用的解決方案,可以對其進行修改以解決多個特定問題,因為它們與特定問題無關。
- 它們富有表現力:設計模式可以非常優雅地解釋大型解決方案。
- 他們簡化了溝通:當開發人員熟悉設計模式時,他們可以更輕鬆地就給定問題的潛在解決方案相互溝通。
- 它們避免了重構代碼的需要:如果在編寫應用程序時考慮到了設計模式,通常情況下您以後不需要重構代碼,因為將正確的設計模式應用於給定的問題已經是最優的了解決方案。
- 它們降低了代碼庫的大小:因為設計模式通常是優雅和最佳的解決方案,它們通常比其他解決方案需要更少的代碼。
我知道你現在已經準備好開始了,但在你學習所有關於設計模式的知識之前,讓我們回顧一些 JavaScript 基礎知識。
JavaScript 簡史
JavaScript 是當今最流行的 Web 開發編程語言之一。 它最初是作為各種顯示的 HTML 元素(稱為客戶端腳本語言)的一種“粘合劑”,用於最初的 Web 瀏覽器之一。 名為 Netscape Navigator,當時只能顯示靜態 HTML。 正如您可能假設的那樣,這種腳本語言的想法導致了當時瀏覽器開發行業的大玩家之間的瀏覽器戰爭,例如 Netscape Communications(今天的 Mozilla)、Microsoft 和其他公司。
每個大玩家都想推動他們自己實現這種腳本語言,所以 Netscape 製作了 JavaScript(實際上,Brendan Eich 做了),微軟製作了 JScript,等等。 如您所見,這些實現之間的差異很大,因此 Web 瀏覽器的開發是針對每個瀏覽器進行的,並帶有網頁附帶的最佳查看貼紙。 很快就清楚我們需要一個標準的、跨瀏覽器的解決方案來統一開發過程並簡化網頁的創建。 他們提出的稱為 ECMAScript。
ECMAScript 是所有現代瀏覽器都試圖支持的標準化腳本語言規範,並且 ECMAScript 有多種實現(可以說是方言)。 最受歡迎的是本文的主題,JavaScript。 自最初發布以來,ECMAScript 已經標準化了許多重要的事情,對於那些對細節更感興趣的人,維基百科上提供了每個版本的 ECMAScript 標準化項目的詳細列表。 瀏覽器對 ECMAScript 版本 6 (ES6) 及更高版本的支持仍然不完整,必須轉換為 ES5 才能得到完全支持。
什麼是 JavaScript?
為了全面掌握本文的內容,讓我們先介紹一些非常重要的語言特性,在深入了解 JavaScript 設計模式之前,我們需要了解這些特性。 如果有人問你“什麼是 JavaScript?” 您可能會在以下幾行中回答:
JavaScript 是一種輕量級、解釋型、面向對象的編程語言,具有一流的功能,通常被稱為網頁腳本語言。
上述定義的意思是說 JavaScript 代碼具有低內存佔用、易於實現、易於學習,其語法類似於 C++ 和 Java 等流行語言。 它是一種腳本語言,這意味著它的代碼被解釋而不是編譯。 它支持過程式、面向對象和函數式編程風格,這使得它對開發人員來說非常靈活。
到目前為止,我們已經了解了聽起來與許多其他語言相似的所有特徵,所以讓我們來看看 JavaScript 與其他語言有關的具體情況。 我將列出一些特徵,並儘我所能解釋為什麼它們值得特別關注。
JavaScript 支持一流的函數
當我剛開始接觸 JavaScript 時,這個特性讓我很難掌握,因為我來自 C/C++ 背景。 JavaScript 將函數視為一等公民,這意味著您可以將函數作為參數傳遞給其他函數,就像傳遞任何其他變量一樣。
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
JavaScript 是基於原型的
與許多其他面向對象的語言一樣,JavaScript 支持對象,在考慮對象時首先想到的術語之一是類和繼承。 這是它變得有點棘手的地方,因為該語言不支持其簡單語言形式的類,而是使用稱為基於原型或基於實例的繼承的東西。
剛剛在 ES6 中引入了正式的術語類,這意味著瀏覽器仍然不支持這個(如果你還記得,在撰寫本文時,最後一個完全支持的 ECMAScript 版本是 5.1)。 然而,重要的是要注意,即使“類”這個詞被引入到 JavaScript 中,它仍然在底層使用基於原型的繼承。
基於原型的編程是一種面向對象的編程風格,其中行為重用(稱為繼承)是通過作為原型的委託重用現有對象的過程來執行的。 一旦我們進入本文的設計模式部分,我們將更詳細地了解這一點,因為這個特性在很多 JavaScript 設計模式中都有使用。
JavaScript 事件循環
如果您有使用 JavaScript 的經驗,那麼您肯定熟悉術語回調函數。 對於那些不熟悉該術語的人來說,回調函數是作為參數(請記住,JavaScript 將函數視為一等公民)發送給另一個函數並在事件觸發後執行的函數。 這通常用於訂閱事件,例如鼠標單擊或鍵盤按鈕按下。
每次帶有監聽器的事件觸發(否則事件丟失)時,一條消息被發送到正在同步處理的消息隊列中,以 FIFO 方式(先進先出)。 這稱為事件循環。
隊列中的每條消息都有一個與之關聯的函數。 一旦消息出隊,運行時會在處理任何其他消息之前完全執行該函數。 也就是說,如果一個函數包含其他函數調用,它們都是在處理隊列中的新消息之前執行的。 這稱為運行至完成。
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage()
同步等待新消息。 每個正在處理的消息都有自己的堆棧,並且一直處理到堆棧為空。 完成後,將從隊列中處理一條新消息(如果有的話)。
您可能還聽說過 JavaScript 是非阻塞的,這意味著當執行異步操作時,程序可以在等待異步操作完成的同時處理其他事情,例如接收用戶輸入,而不是阻塞 main執行線程。 這是 JavaScript 的一個非常有用的屬性,整篇文章都可以寫在這個主題上; 但是,它超出了本文的範圍。
什麼是設計模式?
正如我之前所說,設計模式是軟件設計中常見問題的可重用解決方案。 讓我們看一下設計模式的一些類別。
原型模式
如何創建一個模式? 假設您發現了一個常見問題,並且您有自己獨特的解決方案來解決這個問題,但該解決方案並未在全球範圍內得到認可和記錄。 每次遇到此問題時,您都會使用此解決方案,並且您認為它是可重用的,並且開發人員社區可以從中受益。
它是否立即成為一種模式? 幸運的是,沒有。 很多時候,一個人可能有很好的代碼編寫習慣,只是把看起來像模式的東西誤認為是一種模式,而實際上它不是一種模式。
你怎麼知道你認為你認識的實際上是一種設計模式?
通過了解其他開發人員的意見,了解創建模式本身的過程,並讓自己熟悉現有模式。 模式在成為成熟模式之前必須經過一個階段,這稱為原型模式。
如果原型模式通過了各種開發人員和場景的一定時期的測試,並且該模式被證明是有用的並給出正確的結果,那麼它就是一個未來的模式。 有相當多的工作和文檔——其中大部分超出了本文的範圍——需要完成,以便形成一個被社區認可的成熟模式。
反模式
設計模式代表了良好的實踐,反模式則代表了不好的實踐。
反模式的一個例子是修改Object
類原型。 JavaScript 中幾乎所有的對像都繼承自Object
(請記住 JavaScript 使用基於原型的繼承),因此想像一下您更改了此原型的場景。 從這個原型繼承的所有對像中都可以看到對Object
原型的更改——這將是大多數JavaScript 對象。 這是一場等待發生的災難。
另一個與上述類似的示例是修改您不擁有的對象。 這方面的一個例子是覆蓋整個應用程序中許多場景中使用的對象的函數。 如果你和一個大團隊一起工作,想像一下這會造成什麼混亂; 您很快就會遇到命名衝突、不兼容的實現和維護噩夢。
與了解所有好的做法和解決方案的用處類似,了解不好的做法和解決方案也非常重要。 這樣,您可以識別它們並避免預先犯錯誤。
設計模式分類
設計模式可以按多種方式分類,但最流行的一種是:
- 創造型設計模式
- 結構設計模式
- 行為設計模式
- 並發設計模式
- 架構設計模式
創意設計模式
這些模式處理與基本方法相比優化對象創建的對象創建機制。 對象創建的基本形式可能會導致設計問題或增加設計的複雜性。 創建型設計模式通過某種方式控制對象創建來解決這個問題。 此類別中一些流行的設計模式是:
- 工廠方法
- 抽象工廠
- 建造者
- 原型
- 辛格爾頓
結構設計模式
這些模式處理對象關係。 他們確保如果系統的某個部分發生變化,整個系統不需要隨之變化。 此類別中最流行的模式是:
- 適配器
- 橋
- 合成的
- 裝飾器
- 正面
- 蠅量級
- 代理
行為設計模式
這些類型的模式識別、實現和改進系統中不同對象之間的通信。 它們有助於確保系統的不同部分具有同步信息。 這些模式的流行示例是:
- 責任鏈
- 命令
- 迭代器
- 調解員
- 紀念
- 觀察者
- 狀態
- 戰略
- 遊客
並發設計模式
這些類型的設計模式處理多線程編程範例。 一些流行的是:
- 活動對象
- 核反應
- 調度器
建築設計模式
用於架構目的的設計模式。 一些最著名的是:
- MVC(模型-視圖-控制器)
- MVP(模型-視圖-演示者)
- MVVM(模型-視圖-視圖模型)
在下一節中,我們將仔細研究上述一些設計模式,並提供示例以更好地理解。
設計模式示例
每個設計模式都代表特定類型問題的特定類型解決方案。 沒有一套通用的模式總是最合適的。 我們需要了解特定模式何時會被證明是有用的,以及它是否會提供實際價值。 一旦我們熟悉了它們最適合的模式和場景,我們就可以輕鬆確定特定模式是否適合給定問題。
請記住,將錯誤的模式應用於給定問題可能會導致不良影響,例如不必要的代碼複雜性、不必要的性能開銷,甚至產生新的反模式。
在考慮將設計模式應用於我們的代碼時,這些都是需要考慮的重要事項。 我們將看一下我個人認為有用的一些設計模式,並相信每個高級 JavaScript 開發人員都應該熟悉。
構造函數模式
在考慮經典的面向對象語言時,構造函數是類中的一個特殊函數,它使用一組默認值和/或傳入值初始化對象。
在 JavaScript 中創建對象的常用方法有以下三種:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
創建對像後,有四種方法(從 ES3 開始)向這些對象添加屬性。 它們是:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
最流行的創建對象的方法是大括號,以及用於添加屬性的點符號或方括號。 任何有 JavaScript 經驗的人都使用過它們。
我們之前提到 JavaScript 不支持原生類,但它通過在函數調用前使用“new”關鍵字來支持構造函數。 這樣,我們可以將函數用作構造函數並初始化其屬性,就像使用經典語言構造函數一樣。
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
但是,這裡仍有改進的空間。 如果你還記得,我之前提到過 JavaScript 使用基於原型的繼承。 前一種方法的問題是方法writesCode
為Person
構造函數的每個實例重新定義。 我們可以通過將方法設置到函數原型中來避免這種情況:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
現在, Person
構造函數的兩個實例都可以訪問writesCode()
方法的共享實例。
模塊模式
就特性而言,JavaScript 從未停止過令人驚奇的事情。 JavaScript 的另一個特殊之處(至少就面向對象的語言而言)是 JavaScript 不支持訪問修飾符。 在經典的 OOP 語言中,用戶定義一個類並確定其成員的訪問權限。 由於純 JavaScript 既不支持類也不支持訪問修飾符,因此 JavaScript 開發人員想出了一種在需要時模仿這種行為的方法。
在我們進入模塊模式細節之前,讓我們先談談閉包的概念。 閉包是一個可以訪問父作用域的函數,即使在父函數關閉之後也是如此。 它們幫助我們通過作用域來模仿訪問修飾符的行為。 讓我們通過一個例子來展示這一點:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
如您所見,通過使用 IIFE,我們將 counter 變量綁定到一個函數,該函數被調用並關閉,但仍可以被遞增它的子函數訪問。 由於我們無法從函數表達式外部訪問計數器變量,因此我們通過範圍操作將其設為私有。
使用閉包,我們可以創建具有私有和公共部分的對象。 這些被稱為模塊,當我們想要隱藏對象的某些部分並且只向模塊的用戶公開接口時,它們非常有用。 讓我們在一個例子中展示這一點:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
這種模式引入的最有用的東西是對象的私有部分和公共部分的明確分離,這是一個非常類似於來自經典面向對象背景的開發人員的概念。
然而,並非一切都如此完美。 當您希望更改成員的可見性時,由於訪問公共部分和私有部分的性質不同,您需要在使用該成員的任何地方修改代碼。 此外,在創建後添加到對象的方法不能訪問對象的私有成員。

揭示模塊模式
該模式是對上述模塊模式的改進。 主要區別在於我們在模塊的私有範圍內編寫了整個對象邏輯,然後通過返回一個匿名對象來簡單地公開我們想要公開的部分。 我們還可以在將私有成員映射到其對應的公共成員時更改私有成員的命名。
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
揭示模塊模式是我們可以實現模塊模式的至少三種方式之一。 顯示模塊模式與模塊模式的其他變體之間的區別主要在於如何引用公共成員。 因此,顯示模塊模式更易於使用和修改; 但是,它在某些情況下可能會被證明是脆弱的,例如在繼承鏈中使用 RMP 對像作為原型。 有問題的情況如下:
- 如果我們有一個引用公共函數的私有函數,我們不能覆蓋公共函數,因為私有函數將繼續引用該函數的私有實現,從而在我們的系統中引入一個錯誤。
- 如果我們有一個公共成員指向一個私有變量,並試圖從模塊外部覆蓋公共成員,其他函數仍然會引用該變量的私有值,從而在我們的系統中引入一個錯誤。
單例模式
當我們只需要一個類的一個實例時,就會使用單例模式。 例如,我們需要一個包含某些配置的對象。 在這些情況下,只要係統中某處需要配置對象,就不必創建新對象。
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
正如您在示例中看到的,生成的隨機數始終相同,以及發送的配置值。
需要注意的是,用於檢索單例值的訪問點只需一個並且眾所周知。 使用這種模式的一個缺點是測試起來相當困難。
觀察者模式
當我們需要以優化的方式改進系統不同部分之間的通信時,觀察者模式是一個非常有用的工具。 它促進了對象之間的鬆散耦合。
該模式有多種版本,但在其最基本的形式中,我們有該模式的兩個主要部分。 第一個是主體,第二個是觀察者。
主題處理與觀察者訂閱的某個主題有關的所有操作。 這些操作為觀察者訂閱某個主題,取消訂閱某個主題的觀察者,並在發布事件時通知觀察者有關某個主題。
但是,這種模式有一個變體,稱為發布者/訂閱者模式,我將在本節中將其用作示例。 經典觀察者模式和發布者/訂閱者模式之間的主要區別在於,發布者/訂閱者比觀察者模式促進了更鬆散的耦合。
在觀察者模式中,主體持有對訂閱觀察者的引用,並直接從對象本身調用方法,而在發布者/訂閱者模式中,我們有通道,它充當訂閱者和發布者之間的通信橋樑。 發布者觸發一個事件並簡單地執行為該事件發送的回調函數。
我將展示一個發布者/訂閱者模式的簡短示例,但是對於那些感興趣的人,可以很容易地在網上找到一個經典的觀察者模式示例。
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
這種設計模式在我們需要對被觸發的單個事件執行多個操作的情況下很有用。 假設您有一個場景,我們需要對後端服務進行多次 AJAX 調用,然後根據結果執行其他 AJAX 調用。 您將不得不將 AJAX 調用嵌套在另一個中,這可能會進入一種稱為回調地獄的情況。 使用發布者/訂閱者模式是一個更優雅的解決方案。
使用這種模式的一個缺點是很難測試我們系統的各個部分。 我們沒有優雅的方法可以知道系統的訂閱部分是否按預期運行。
中介者模式
我們將簡要介紹一種在討論解耦系統時也非常有用的模式。 當我們有一個系統的多個部分需要通信和協調的場景時,也許一個好的解決方案是引入一個中介。
中介者是一個對象,它用作系統不同部分之間通信的中心點,並處理它們之間的工作流。 現在,重要的是要強調它處理工作流程。 為什麼這很重要?
因為與發布者/訂閱者模式有很大的相似性。 你可能會問自己,好的,所以這兩種模式都有助於實現對象之間更好的通信……有什麼區別?
不同之處在於中介者處理工作流程,而發布者/訂閱者使用一種稱為“即發即棄”的通信類型。 發布者/訂閱者只是一個事件聚合器,這意味著它只是負責觸發事件並讓正確的訂閱者知道觸發了哪些事件。 事件聚合器不關心觸發事件後會發生什麼,而調解器則不然。
調解器的一個很好的例子是嚮導類型的接口。 假設您有一個龐大的系統註冊流程。 通常,當需要用戶提供大量信息時,最好將其分解為多個步驟。
這樣,代碼將更加簡潔(更易於維護),並且用戶不會被僅僅為了完成註冊而請求的大量信息所淹沒。 中介是一個處理註冊步驟的對象,考慮到由於每個用戶可能具有唯一的註冊過程而可能發生的不同可能的工作流程。
這種設計模式的明顯好處是改進了系統不同部分之間的通信,現在它們都通過中介和更清晰的代碼庫進行通信。
不利的一面是,現在我們在系統中引入了單點故障,這意味著如果我們的中介發生故障,整個系統可能會停止工作。
原型模式
正如我們在整篇文章中已經提到的,JavaScript 不支持原生形式的類。 對象之間的繼承是使用基於原型的編程實現的。
它使我們能夠創建對象,這些對象可以作為正在創建的其他對象的原型。 原型對像用作構造函數創建的每個對象的藍圖。
正如我們在前幾節中已經討論過的那樣,讓我們展示一個如何使用這種模式的簡單示例。
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
下一步
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.