JavaScript 的 7 種設計模式
當啟動一個新的項目時候,我們不應該馬上開始編程。而是首先應該定義項目的目的和范圍,然后列出其功能或規(guī)格。如果你已經(jīng)開始編程或者正在從事一個復雜的項目,則應該選擇一個最適合你項目的設計模式。
什么是設計模式?
在軟件工程中,設計模式是針對軟件設計中常見問題的可重用解決方案。設計模式也是經(jīng)驗豐富的開發(fā)人員針對特定問題的最佳實踐。它可以被當作編程的模板。
為什么要使用設計模式?
許多工程師要么認為設計模式浪費時間,要么不知道如何恰當?shù)氖褂迷O計模式。但如果能正確使用設計模式,則可以幫助你寫出更好的可讀性更高的代碼,并且代碼更容易被維護和理解。
最重要的是,設計模式為軟件開發(fā)人員提供了通用的詞匯表。它們能讓學習你代碼的人很快了解代碼的意圖。例如,如果你的項目中使用了裝飾器模式,那么新的開發(fā)可以很快就知道這段代碼的作用,從而他們可以將更多精力放在解決業(yè)務問題上,而不是試圖理解代碼在做什么。
我們已經(jīng)知道了什么是設計模式和它的重要性,下面我們深入研究一下 JavaScript 中的 7 種設計模式。
一、模塊模式
模塊是一段獨立的代碼,因此我們可以更新模塊而不會影響代碼的其它部分。模塊還允許我們通過為變量創(chuàng)建單獨的作用域來避免命名空間污染。當它們與其它代碼解耦時,我們還可以在其它項目中重用模塊。
模塊是任何現(xiàn)代 JavaScript 應用程序不可或缺的一部分,有助于保持代碼干凈,獨立和有條理。在 JavaScript 中有許多方法可以創(chuàng)建模塊,其中一種是模塊模式。
與其它編程語言不同,JavaScript 沒有訪問修飾符,也就是說,你不能將變量聲明為私有的或公共的。因此,模塊模式也可用來模擬封裝的概念。
模塊模式使用 IIFE(立即調用的函數(shù)表達式),閉包和函數(shù)作用域來模擬封裝的概念。例如:
- const myModule = (function() {
- const privateVariable = 'Hello World';
- function privateMethod() {
- console.log(privateVariable);
- }
- return {
- publicMethod: function() {
- privateMethod();
- }
- }
- })();
- myModule.publicMethod();
由于是 IIFE 因此代碼會被立即執(zhí)行,并將返回對象賦值給了 myModule 變量。由于閉包,即使在 IIFE 完成后,返回的對象仍可以訪問 IIFE 內部定義的函數(shù)和變量。
因此,IIFE 內部定義的變量和函數(shù)對外部是看不見的,從而使其成為 myModule 模塊的私有成員。
執(zhí)行代碼后,myModule 變量看起來像下面所示:
- const myModule = {
- publicMethod: function() {
- privateMethod();
- }};
因此當我們調用 publicMethod() 時候,它將調用 privateMethod() 例如:
- // Prints 'Hello World'
- module.publicMethod();
二、揭示模塊模式
揭示模塊模式是 Christian Heilmann 對模塊模式的略微改進。模塊模式的問題在于,我們必須創(chuàng)建新的公共函數(shù)才能調用私有函數(shù)和變量。
在這種模式下,我們將返回的對象的屬性映射到要公開暴露的私有函數(shù)上。這就是為什么將其稱為揭示模塊模式。例如:
- 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();
這種模式讓我們更容易知道哪些函數(shù)和變量是公共的,無形中提高了代碼的可讀性。執(zhí)行代碼后 myRevealingModule 看起來像下所示:
- const myRevealingModule = {
- setName: publicSetName,
- greeting: publicVar,
- getName: publicGetName
- };
當我們調用 myRevealingModule.setName('Mark') 時,實際調用了內部的 publicSetName。當調用 myRevealingModule.getName() 時,實際調用了內部的 publicGetName 例如:
- myRevealingModule.setName('Mark');
- // prints Name: Mark
- myRevealingModule.getName();
與模塊模式相比,揭示模塊模式的優(yōu)勢有:
- 通過修改 return 語句中的一行,我們可以將成員從公共變?yōu)闉樗饺耍粗嗳弧?/li>
- 返回的對象不包含任何函數(shù)定義,所有右側表達式都在 IIFE 中定義,從而使代碼清晰易讀。
三、ES6 模塊
在 ES6 之前,JavaScript 沒有內置模塊,因此開發(fā)人員必須依靠第三方庫或模塊模式來實現(xiàn)模塊。但是自從 ES6,JavaScript 內置了模塊。
ES6 的模塊是以文件形式存儲的。每個文件只能有一個模塊。默認情況下,模塊內的所有內容都是私有的。通過使用 export 關鍵字來暴露函數(shù)、變量和類。模塊內的代碼始終在嚴格模式下運行。
3.1 導出模塊
有兩種方法可以導出函數(shù)和變量聲明:
- 在函數(shù)和變量聲明的前面添加 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 關鍵字來暴露函數(shù)和變量。例如:
- // 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};
3.2 導入模塊
與導出模塊相似,有兩種使用 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));
3.3 導入導出中使用別名
- 重命名導出
- // 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));
四、單例模式
一個單例對象是只能實例化一次的對象。如果不存在,則單例模式將創(chuàng)建類的新實例。如果存在實例,則僅返回對該對象的引用。重復調用構造函數(shù)將始終獲取同一對象。
JavaScript 是一直內置單例的語言。我們只是不稱它們?yōu)閱卫?,我們稱它們?yōu)閷ο笞置媪?。例如?/p>
- 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);
可以使用構造函數(shù)來實現(xiàn)單例模式。例如:
- 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);
調用此構造函數(shù)時,它將檢查 instance 對象是否存在。如果對象不存在,則將 this 變量分配給 instance 變量。如果該對象存在,則只返回該對象。
單例也可以使用模塊模式來實現(xiàn)。例如:
- 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 方法來創(chuàng)建一個新實例。如果實例已經(jīng)存在,則此方法僅返回該實例。如果該實例不存在,則通過調用該 init() 函數(shù)創(chuàng)建一個新實例。
五、工廠模式
工廠模式使用工廠方法創(chuàng)建對象而不需要指定具體的類或構造函數(shù)的模式。
工廠模式用于創(chuàng)建對象而不需要暴露實例化的邏輯。當我們需要根據(jù)特定條件生成不同的對象時,可以使用此模式。例如:
- 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);
- }
- }
- }
這里,創(chuàng)建了一個 Car 和一個 Truck 類(具有一些默認值),該類用于創(chuàng)建新的 car 和 truck對象。而且定義了一個VehicleFactory 類,用來根據(jù) options 對象中的 vehicleType 屬性來創(chuàng)建和返回新的對象。
- 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 創(chuàng)建了一個新的 factory 對象。然后,我們通過調用 factory.createVehicle 方法并且傳遞 options 對象,其 vehicleType 屬性可能為 car 或者 truck 來創(chuàng)建新 Car 或 Truck 對象。
六、裝飾器模式
裝飾器模式用于擴展對象的功能,而無需修改現(xiàn)有的類或構造函數(shù)。此模式可用于將特征添加到對象中,而無需修改底層的代碼。
此模式的一個簡單示例為:
- 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);
這種模式的一個更實際的例子是:
假設汽車的成本取決于其功能的數(shù)量。如果沒有裝飾器模式,我們將不得不為不同的功能組合創(chuàng)建不同的類,每個類都有一個 cost 方法來計算成本。例如:
- class Car() {
- }
- class CarWithAC() {
- }
- class CarWithAutoTransmission {
- }
- class CarWithPowerLocks {
- }
- class CarWithACandPowerLocks {
- }
但是,通過裝飾器模式,我們可以創(chuàng)建一個基類 car 并且通過裝飾器函數(shù)給不同的對象添加對應的成本邏輯。
- 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;
- }
- }
首先,我們創(chuàng)建了小轎車的基類 Car。然后針對要添加的特性創(chuàng)建了裝飾器并且此裝飾器以 Car 對象為參數(shù)。然后通過返回更新后的小汽車成本來覆蓋對象的成本函數(shù),且添加了一個用來標識某個特性是否已經(jīng)被添加的屬性。
要添加新的功能,我們只需要像下面一樣就可以:
- const car = new Car();
- console.log(car.cost());
- carWithAC(car);
- carWithAutoTransmission(car);
- carWithPowerLocks(car);
最后,我們可以像這樣計算汽車的成本:
- // Calculating total cost of the car
- console.log(car.cost());
結論
我們已經(jīng)了解了 JavaScript 中使用的各種設計模式,但是這里沒有涉及到可以用 JavaScript 實現(xiàn)的設計模式。
盡管了解各種設計模式很重要,但不要過度使用它們也同樣重要。在使用設計模式之前,你應該仔細考慮你的問題是否適合該設計模式。要知道某個模式是否適合你的問題,應該好好研究該設計模式以及它的應用。