理解 JavaScript 中的設計模式
小編推薦: ofollow,noindex">掘金是一個面向程式員的高質量技術社群,從 一線大廠經驗分享到前端開發最佳實踐,無論是入門還是進階,來掘金你不會錯過前端開發的任何一個技術乾貨。
本文幫助你瞭解 JavaScript 中的常用的幾種設計模式。
當你開始一個新專案時,你不會立即開始編碼。 首先必須定義專案的目的和範圍,然後列出專案功能或專案說明書。 在你可以開始編碼或者你正在處理更復雜的專案之後,你應該選擇最適合你專案的設計模式。
什麼是設計模式?
在軟體工程中,設計模式是軟體設計中常見問題可重用的解決方案。設計模式代表著經驗豐富的軟體開發人員使用的最佳實踐。設計模式可以被認為是程式設計模板。
為什麼使用設計模式 ?
許多程式員要麼認為設計模式是浪費時間,要麼他們不知道如何恰當地應用它們。 但是使用適當的設計模式可以幫助你編寫更好,更易理解的程式碼,並且程式碼可以輕鬆維護,因為它更容易理解。
最重要的是,設計模式為軟體開發人員提供了一些溝通上的便利。 它們會立即向學習你程式碼的人顯示你的程式碼的意圖。
例如,如果你在專案中使用裝飾者模式,那麼新程式設計師將立即知道該程式碼正在做什麼,並且他們可以更專注於解決業務問題,而無需花費精力去理解你的程式碼正在做什麼。
現在我們知道了什麼是設計模式,以及它們為什麼重要,讓我們深入研究 JavaScript 中使用的各種設計模式。
模組模式(Module Pattern)
模組是一段獨立的程式碼,因此我們可以在不影響其他程式碼的情況下單獨更新模組。 模組還允許我們為變數建立單獨的作用域來避免名稱空間的汙染。 當它們與其他程式碼段分離時,我們也可以在其他專案中重用模組。
模組是任何現代 JavaScript 應用程式不可或缺的一部分,有助於保持程式碼清潔,分離和組織。 有許多方法可以在JavaScript 中建立模組,其中一種是模組模式。
Bit之類的平臺可以幫助將模組和元件轉換為共享的構建塊,可以與任何專案共享,發現和開發。 通過零重構,它是一種快速且可擴充套件的方式來共享和重用程式碼。
與其他程式語言不同,JavaScript 沒有訪問修飾符的特性,也就是說,你不能將變數宣告為私有(private)或公開(public)。 因此模組模式也常常被用於模擬封裝的概念。
此模式使用IIFE(立即呼叫的函式表示式),閉包和函式作用域來模擬此概念。 例如:
const myModule = (function() { const privateVariable = 'Hello World'; function privateMethod() { console.log(privateVariable); } return { publicMethod: function() { privateMethod(); } } })(); myModule.publicMethod();
由於上面的程式碼是IIFE,程式碼會立即執行,返回的物件被分配給 myModule
變數。 由於閉包,即使在IIFE完成之後,返回的物件仍然可以訪問 IIFE 內定義的函式和變數。
因此,在 IIFE 中定義的變數和函式對外部作用域來說基本上是隱藏的,因此它們對 myModule
變數是私有的。
執行程式碼後, myModule
變數如下所示:
const myModule = { publicMethod: function() { privateMethod(); } };
因此,我們可以呼叫 publicMethod()
,轉而呼叫 privateMethod()
。例如:
// Prints 'Hello World' module.publicMethod();
暴露模組模式(Revealing Module Pattern)
暴露模組模式是 Christian Heilmann 對模組模式略微的改進版本。 模組模式的問題是我們必須建立新的公開函式來呼叫私有函式和變數。
在暴露模組模式中,我們將返回的物件的屬性對映到我們想要公開的私有函式。 這就是為什麼它被稱為暴露模組模式的原因。 例如:
const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar= 'Hello World'; function privateFunction() { console.log('Name: '+ privateVar); } function publicSetName(name) { privateVar = name; } function publicGetName() { privateFunction(); } /** reveal methods and variables by assigning them to objectproperties */ return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName('Mark'); // prints Name: Mark myRevealingModule.getName();
這種模式使我們更容易理解我們可以公開訪問哪些函式和變數,這有助於程式碼的可讀性。
程式碼執行後, myRevealingModule
如下所示:
const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };
我們可以呼叫 myRevealingModule.setName('Mark')
,來引用內部的 publicSetName
,以及呼叫 myRevealingModule.getName()
,來引用內部的 publicGetName
。例如:
myRevealingModule.setName('Mark'); // prints Name: Mark myRevealingModule.getName();
暴露模組模式相較於模組模式的優點:
- 我們可以修改 return 語句中的一行程式碼,來將成員從 public(公開) 更改為 private(私有) ,反之亦然。
- 返回的物件不包含函式定義,所有右側表示式都在 IIFE 中定義,使程式碼清晰易讀。
ES6 模組(ES6 Modules)
在ES6之前,JavaScript 沒有內建的模組系統,所以開發人員必須依賴第三方庫或模組模式來實現模組化。但是在 ES6 中,JavaScript 擁有了原生的模組系統。
ES6 模組儲存在單獨的檔案中。每個檔案只能有一個模組。預設情況下,模組中的所有內容都是私有的。函式、變數和類使用 export
關鍵字來向外公開。模組內的程式碼總是在 嚴格模式(strict mode) 下執行。
匯出模組
匯出函式和變數宣告有兩種方法:
- 1) 通過在函式和變數宣告前新增
export
關鍵字。例如:
// utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } // This is a private function function privateLog() { console.log('Private Function'); }
- 2) 通過在程式碼末尾新增
export
關鍵字,幷包含我們要匯出的函式和變數的名稱。例如:
// utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } // This is a private function function privateLog() { console.log('Private Function'); } export {multiply, divide};
匯入模組
與匯出模組類似,有兩種方法可以使用 import
關鍵字匯入模組。 例如:
- 1) 一次匯入多個專案
// main.js // importing multiple items import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7));
匯入所有模組
// main.js // importing all of module import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7));
匯入/匯出模組可以使用別名
如果要避免命名衝突,可以在匯出和匯入時使用別名。例如:
- 1)重新命名匯出
// utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply};
- 2) 重新命名匯入
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7));
你可以檢視JavaScript 模組簡史 和 ECMAScript 6 Modules(模組)系統及語法詳解 來完整了解 JavaScript 模組化程序和 ES6 Modules(模組)的更多資訊。
單例模式(Singleton Pattern)
Singleton(單例) 是一個只能例項化一次的物件。 如果不存在,則單例模式會建立類的新例項。 如果存在例項,則它只返回對該物件的引用。 對建構函式的任何重複呼叫總是會獲取相同的物件。
JavaScript 一直支援單例模式。 我們只是不稱他們為單例,我們稱之為 物件字面量。 例如:
const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() { console.log('Hello!'); } };
因為 JavaScript 中的每個物件佔用一個唯一的記憶體位置,當我們呼叫 user
物件時,我們實際上是返回對該物件的引用。
如果我們嘗試將使用者變數複製到另一個變數並修改該變數。 例如:
const user1 = user; user1.name = 'Mark';
我們會看到的結果是兩個物件都被修改,因為 JavaScript 中的物件是通過引用而不是通過值傳遞的。所以記憶體中只有一個物件。例如:
// prints 'Mark' console.log(user.name); // prints 'Mark' console.log(user1.name); // prints true console.log(user === user1);
可以使用建構函式實現單例模式。例如:
let instance = null; function User() { if(instance) { return instance; } instance = this; this.name = 'Peter'; this.age = 25; return instance; } const user1 = new User(); const user2 = new User(); // prints true console.log(user1 === user2);
呼叫此建構函式時,它會檢查 instance
物件是否存在。 如果該物件不存在,則將該變數分配給 instance
變數。如果物件存在,它只返回該物件。
單例模式也可以使用模組模式實現。 例如:
const singleton = (function() { let instance; function init() { return { name: 'Peter', age: 24, }; } return { getInstance: function() { if(!instance) { instance = init(); } return instance; } } })(); const instanceA = singleton.getInstance(); const instanceB = singleton.getInstance(); // prints true console.log(instanceA === instanceB);
在上面的程式碼中,我們通過呼叫 singleton.getInstance
方法建立一個新例項。 如果例項已存在,則此方法僅返回該例項,如果例項不存在,則通過呼叫 init()
函式建立新例項。
工廠模式(Factory Pattern)
工廠模式是一種使用工廠方法建立物件的設計模式,而不指定建立物件的確切的類或建構函式。
工廠模式用於在不公開例項化邏輯的情況下建立物件。當我們需要根據特定條件生成不同的物件時,可以使用此模式。例如:
class Car{ constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'brand new'; this.color = options.color || 'white'; } } class Truck { constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'used'; this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) { if(options.vehicleType === 'car') { return new Car(options); } else if(options.vehicleType === 'truck') { return new Truck(options); } } }
在這裡,我建立了一個 Car
和 Truck
類(帶有一些預設值),用於建立新的 car 和 truck 物件。 我已經定義了一個 VehicleFactory
類,用於根據 options
物件中收到的 vehicleType
屬性建立並返回一個新物件。
const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); // Prints Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // Prints Truck {doors: 2, state: "used", color: "white"} console.log(truck);
我建立了一個 VehicleFactory
類的新物件 factory
。之後,我們可以通過呼叫 factory.createVehicle
並,傳遞一個帶有 carType
屬性 options
物件,且值為 car
或 truck
的來建立一個新的 Car
或 Truck
物件。
裝飾者模式(Decorator Pattern)
裝飾者模式用於擴充套件物件的功能,而無需修改現有的類或建構函式。 此模式可用於向物件新增功能,而無需它們修改底層程式碼。
這種模式的一個簡單例子是:
function Car(name) { this.name = name; // Default values this.color = 'White'; } // Creating a new Object to decorate const tesla= new Car('Tesla Model 3'); // Decorating the object with new functionality tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); // prints black console.log(tesla.color);
這種模式的一個更實際的例子是:
比方說,汽車的成本取決於它的功能數量。 如果沒有裝飾者模式,我們必須為不同的功能組合建立不同的類,每個類都有一個成本方法來計算成本。 例如:
class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }
但是使用裝飾者模式,我們可以建立一個基類 ·
Car`,並使用裝飾者函式將不同配置的成本計算方法新增到其物件中。例如:
class Car { constructor() { // Default Cost this.cost = function() { return 20000; } } } // Decorator function function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // Decorator function function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // Decorator function function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }
首先,我們建立一個基類 Car
,用於建立 Car
物件。 然後,然後我們為了不同的功能建立了裝飾者函式,並將 Car
物件作為引數傳遞。 然後我們覆蓋該物件的成本函式,該函式返回汽車的更新成本,並向該物件新增新屬性以指示添加了哪個特徵。
要新增新功能,我們可以執行以下操作:
const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car);
最後,我們可以像這樣計算汽車的成本:
// Calculating total cost of the car console.log(car.cost());
結語
我們已經瞭解了JavaScript中使用的各種設計模式,但是這裡還一些沒有介紹的,可以用 JavaScript 實現的設計模式。
雖然瞭解各種設計模式很重要,但同樣重要的是不要過度使用它們。 在使用設計模式之前,你應該仔細考慮你所處的問題是否符合該設計模式。 要了解模式是否適合你的問題,你應該研究設計模式的思想,以及該設計模式的應用。
英文原文:https://blog.bitsrc.io/understanding-design-patterns-in-javascript-13345223f2dd
如果你覺得本文對你有幫助,那就請分享給更多的朋友
關注「前端乾貨精選」加星星,每天都能獲取前端乾貨
