JavaScript 的 7 種設計模式

削微寒發表於2021-02-18

原文地址:Understanding Design Patterns in JavaScript

原文作者:Sukhjinder Arora

譯者:HelloGitHub-Robert

當啟動一個新的專案時候,我們不應該馬上開始程式設計。而是首先應該定義專案的目的和範圍,然後列出其功能或規格。如果你已經開始程式設計或者正在從事一個複雜的專案,則應該選擇一個最適合你專案的設計模式。

什麼是設計模式?

在軟體工程中,設計模式是針對軟體設計中常見問題的可重用解決方案。設計模式也是經驗豐富的開發人員針對特定問題的最佳實踐。它可以被當作程式設計的模板。

為什麼要使用設計模式?

許多工程師要麼認為設計模式浪費時間,要麼不知道如何恰當的使用設計模式。但如果能正確使用設計模式,則可以幫助你寫出更好的可讀性更高的程式碼,並且程式碼更容易被維護和理解。

最重要的是,設計模式為軟體開發人員提供了通用的詞彙表。它們能讓學習你程式碼的人很快了解程式碼的意圖。例如,如果你的專案中使用了裝飾器模式,那麼新的開發可以很快就知道這段程式碼的作用,從而他們可以將更多精力放在解決業務問題上,而不是試圖理解程式碼在做什麼。

我們已經知道了什麼是設計模式和它的重要性,下面我們深入研究一下 JavaScript 中的 7 種設計模式。

一、模組模式

模組是一段獨立的程式碼,因此我們可以更新模組而不會影響程式碼的其它部分。模組還允許我們通過為變數建立單獨的作用域來避免名稱空間汙染。當它們與其它程式碼解耦時,我們還可以在其它專案中重用模組。

模組是任何現代 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();

二、揭示模組模式

揭示模組模式是 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 語句中的一行,我們可以將成員從公共變為為私人,反之亦然。
  • 返回的物件不包含任何函式定義,所有右側表示式都在 IIFE 中定義,從而使程式碼清晰易讀。

三、ES6 模組

在 ES6 之前,JavaScript 沒有內建模組,因此開發人員必須依靠第三方庫或模組模式來實現模組。但是自從 ES6,JavaScript 內建了模組。

ES6 的模組是以檔案形式儲存的。每個檔案只能有一個模組。預設情況下,模組內的所有內容都是私有的。通過使用 export 關鍵字來暴露函式、變數和類。模組內的程式碼始終在嚴格模式下執行。

3.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');
}
  • 在程式碼的最後新增 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};

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));

四、單例模式

一個單例物件是隻能例項化一次的物件。如果不存在,則單例模式將建立類的新例項。如果存在例項,則僅返回對該物件的引用。重複呼叫建構函式將始終獲取同一物件。

JavaScript 是一直內建單例的語言。我們只是不稱它們為單例,我們稱它們為物件字面量。例如:

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() 函式建立一個新例項。

五、工廠模式

工廠模式使用工廠方法建立物件而不需要指定具體的類或建構函式的模式。

工廠模式用於建立物件而不需要暴露例項化的邏輯。當我們需要根據特定條件生成不同的物件時,可以使用此模式。例如:

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 類(具有一些預設值),該類用於建立新的 cartruck 物件。而且定義了一個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 方法並且傳遞 options 物件,其 vehicleType 屬性可能為 car 或者 truck 來建立新 CarTruck 物件。

六、裝飾器模式

裝飾器模式用於擴充套件物件的功能,而無需修改現有的類或建構函式。此模式可用於將特徵新增到物件中,而無需修改底層的程式碼。

此模式的一個簡單示例為:

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);

這種模式的一個更實際的例子是:

假設汽車的成本取決於其功能的數量。如果沒有裝飾器模式,我們將不得不為不同的功能組合建立不同的類,每個類都有一個 cost 方法來計算成本。例如:

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 物件為引數。然後通過返回更新後的小汽車成本來覆蓋物件的成本函式,且新增了一個用來標識某個特性是否已經被新增的屬性。

要新增新的功能,我們只需要像下面一樣就可以:

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 實現的設計模式。

儘管瞭解各種設計模式很重要,但不要過度使用它們也同樣重要。在使用設計模式之前,你應該仔細考慮你的問題是否適合該設計模式。要知道某個模式是否適合你的問題,應該好好研究該設計模式以及它的應用。


關注 HelloGitHub 公眾號 收到第一時間的更新。

還有更多開源專案的介紹和寶藏專案等待你的發掘。

相關文章