JavaScript 的 7 種設計模式

原文地址:Understanding Design Patterns in JavaScriptjavascript

原文做者:Sukhjinder Arorajava

譯者:HelloGitHub-Robertes6

當啓動一個新的項目時候,咱們不該該立刻開始編程。而是首先應該定義項目的目的和範圍,而後列出其功能或規格。若是你已經開始編程或者正在從事一個複雜的項目,則應該選擇一個最適合你項目的設計模式。編程

什麼是設計模式?

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

爲何要使用設計模式?

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

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

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

1、模塊模式

模塊是一段獨立的代碼,所以咱們能夠更新模塊而不會影響代碼的其它部分。模塊還容許咱們經過爲變量建立單獨的做用域來避免命名空間污染。當它們與其它代碼解耦時,咱們還能夠在其它項目中重用模塊。函數

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

2、揭示模塊模式

揭示模塊模式是 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 中定義,從而使代碼清晰易讀。

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

4、單例模式

一個單例對象是隻能實例化一次的對象。若是不存在,則單例模式將建立類的新實例。若是存在實例,則僅返回對該對象的引用。重複調用構造函數將始終獲取同一對象。

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

5、工廠模式

工廠模式使用工廠方法建立對象而不須要指定具體的類或構造函數的模式。

工廠模式用於建立對象而不須要暴露實例化的邏輯。當咱們須要根據特定條件生成不一樣的對象時,可使用此模式。例如:

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 對象。

6、裝飾器模式

裝飾器模式用於擴展對象的功能,而無需修改現有的類或構造函數。此模式可用於將特徵添加到對象中,而無需修改底層的代碼。

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

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 公衆號 收到第一時間的更新。

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

相關文章
相關標籤/搜索