【譯】簡單理解 JavaScript 中的設計模式
前言導讀:這篇文章比較適合初步接觸設計模式的同學,文中介紹了模組模式、單例模式、工廠模式、裝飾者模式,例子都很淺顯易懂。看完之後會對設計模式有一個初步的瞭解。
當你開始一個新的專案時,一般不會立即開始編碼,首先必須要定義專案的目的和範圍,然後列出專案的特性或規格。在可以開始編寫程式碼之後,或者如果你正在處理一個更復雜的專案,那麼你應該選擇一個最適合該專案的設計模式。
什麼是設計模式?
在軟體工程中,設計模式是軟體設計中常見問題的可重用解決方案。設計模式代表了經驗豐富的軟體開發人員所使用的最佳實踐。設計模式可以看作是程式設計模板。
為什麼要使用設計模式?
很多程式員要麼認為設計模式浪費時間,要麼不知道如何恰當地應用它們。但是使用適當的設計模式可以幫助你編寫更好、更易於理解的程式碼,且更容易維護。
最重要的是,設計模式為軟體開發人員提供了一個可以討論的通用詞彙表。它們會立即向學習程式碼的人顯示程式碼的意圖。
例如,如果你在專案中使用裝飾者模式,那麼新加入的開發人員將立即知道這段程式碼在做什麼,所以他們可以更關注於解決業務問題,而不是試圖理解程式碼在做什麼。
現在我們已經知道了什麼是設計模式,以及它們為什麼重要,接下來讓我們深入研究 JavaScript 中使用的各種設計模式。
模組模式-Module Pattern
模組是一段自包含的程式碼,因此我們可以在不影響程式碼其他部分的情況下更新模組。模組還允許我們通過為變數建立單獨的作用域來避免名稱空間汙染。當模組與其他程式碼片段分離時,我們還可以在其他專案中重用它們。
模組是任何現代 JavaScript 應用程式的組成部分,它有助於保持程式碼的整潔、分離和組織。用 JavaScript 建立模組有很多方法,模組模式是其中之一。
與其他程式語言不同,JavaScript沒有訪問修飾符,也就是說,不能將變數宣告為私有或公共。因此,模組模式也被用來模擬封裝的概念。
模組模式使用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 object properties */ 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
關鍵字公開函式、變數和類。模組內的程式碼總是在嚴格模式下執行。
匯出模組
匯出函式和變數宣告有兩種方法:
- 通過在函式和變數宣告前新增
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'); } 複製程式碼
- 通過在包含要匯出的函式和變數名稱的程式碼末尾新增
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
關鍵字匯入模組。例如:
- 一次匯入多個項
// 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)); 複製程式碼
匯入和匯出可以重新命名
如果希望避免命名衝突,可以在匯入和匯出時修改命名,例如:
- 重新命名匯出
// 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}; 複製程式碼
- 重新命名匯入
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7)); 複製程式碼
單例模式-Singleton Pattern
單例物件是隻能例項化一次的物件。如果一個類不存在,單例模式會建立一個新的類例項。如果例項存在,它只返回對該物件的引用。對建構函式的任何重複呼叫都將獲取相同的物件。
在 JavaScript 中,一直都有內建的單例。我們只是不稱它們為單例,而是物件字面量(object literal)。例如:
const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() { console.log('Hello!'); } }; 複製程式碼
因為 JavaScript 中的每個物件都佔用一個唯一的記憶體位置,當我們呼叫 user
物件時,實際上是返回對這個物件的引用。
如果我們試圖將 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
物件是否存在。如果物件不存在,則將 this
變數賦給 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
,傳入帶有 vehicleType
屬性的 options
物件引數,建立了新的 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
物件作為引數傳遞。然後,我們重寫該物件的 cost
函式,該函式返回 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 中實現。
雖然瞭解各種設計模式很重要,但同樣重要的是不要過度使用它們。在使用設計模式之前,你應該仔細考慮你的問題是否符合設計模式。要想知道某個模式是否適合你的問題,請了解該模式解決了哪些問題,並檢查你是否實際面臨類似的問題。
原文: ofollow,noindex">blog.bitsrc.io/understandi…
更多前端內容請關注下方公眾號,您的一點鼓勵就是我極大的動力,希望和大家共同學習:
