【譯】簡單理解 JavaScript 中的設計模式

前言導讀:這篇文章比較適合初步接觸設計模式的同窗,文中介紹了模塊模式、單例模式、工廠模式、裝飾者模式,例子都很淺顯易懂。看完以後會對設計模式有一個初步的瞭解。javascript

當你開始一個新的項目時,通常不會當即開始編碼,首先必需要定義項目的目的和範圍,而後列出項目的特性或規格。在能夠開始編寫代碼以後,或者若是你正在處理一個更復雜的項目,那麼你應該選擇一個最適合該項目的設計模式。前端

什麼是設計模式?

在軟件工程中,設計模式是軟件設計中常見問題的可重用解決方案。設計模式表明了經驗豐富的軟件開發人員所使用的最佳實踐。設計模式能夠看做是編程模板。java

爲何要使用設計模式?

不少程序員要麼認爲設計模式浪費時間,要麼不知道如何恰當地應用它們。可是使用適當的設計模式能夠幫助你編寫更好、更易於理解的代碼,且更容易維護。程序員

最重要的是,設計模式爲軟件開發人員提供了一個能夠討論的通用詞彙表。它們會當即向學習代碼的人顯示代碼的意圖。編程

例如,若是你在項目中使用裝飾者模式,那麼新加入的開發人員將當即知道這段代碼在作什麼,因此他們能夠更關注於解決業務問題,而不是試圖理解代碼在作什麼。設計模式

如今咱們已經知道了什麼是設計模式,以及它們爲何重要,接下來讓咱們深刻研究 JavaScript 中使用的各類設計模式。閉包

模塊模式-Module Pattern

模塊是一段自包含的代碼,所以咱們能夠在不影響代碼其餘部分的狀況下更新模塊。模塊還容許咱們經過爲變量建立單獨的做用域來避免命名空間污染。當模塊與其餘代碼片斷分離時,咱們還能夠在其餘項目中重用它們。編程語言

模塊是任何現代 JavaScript 應用程序的組成部分,它有助於保持代碼的整潔、分離和組織。用 JavaScript 建立模塊有不少方法,模塊模式是其中之一。ide

與其餘編程語言不一樣,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 類(帶有一些默認值),用於建立新的 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,傳入帶有 vehicleType 屬性的 options 對象參數,建立了新的 cartruck 對象。

裝飾者模式-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 中實現。

雖然瞭解各類設計模式很重要,但一樣重要的是不要過分使用它們。在使用設計模式以前,你應該仔細考慮你的問題是否符合設計模式。要想知道某個模式是否適合你的問題,請了解該模式解決了哪些問題,並檢查你是否實際面臨相似的問題。

原文:blog.bitsrc.io/understandi…

更多前端內容請關注下方我我的維護的公衆號,您的一點鼓勵就是我極大的動力,但願和你們共同窗習:

相關文章
相關標籤/搜索