理解 JavaScript 中的設計模式

本文幫助你瞭解 JavaScript 中的經常使用的幾種設計模式。html

當你開始一個新項目時,你不會當即開始編碼。 首先必須定義項目的目的和範圍,而後列出項目功能或項目說明書。 在你能夠開始編碼或者你正在處理更復雜的項目以後,你應該選擇最適合你項目的設計模式。程序員

什麼是設計模式?

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

爲何使用設計模式 ?

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

最重要的是,設計模式爲軟件開發人員提供了一些溝通上的便利。 它們會當即向學習你代碼的人顯示你的代碼的意圖。閉包

例如,若是你在項目中使用裝飾者模式,那麼新程序員將當即知道該代碼正在作什麼,而且他們能夠更專一於解決業務問題,而無需花費精力去理解你的代碼正在作什麼。編程語言

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

模塊模式(Module Pattern)

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

模塊是任何現代 JavaScript 應用程序不可或缺的一部分,有助於保持代碼清潔,分離和組織。 有許多方法能夠在JavaScript 中建立模塊,其中一種是模塊模式。函數

Bit之類的平臺能夠幫助將模塊和組件轉換爲共享的構建塊,能夠與任何項目共享,發現和開發。 經過零重構,它是一種快速且可擴展的方式來共享和重用代碼。學習

與其餘編程語言不一樣,JavaScript 沒有訪問修飾符的特性,也就是說,你不能將變量聲明爲私有(private)或公開(public)。 所以模塊模式也經常被用於模擬封裝的概念。

此模式使用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 關鍵字來向外公開。模塊內的代碼老是在 嚴格模式(strict mode) 下運行。

導出模塊

導出函數和變量聲明有兩種方法:

  • 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');
}
  • 2) 經過在代碼末尾添加 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 關鍵字導入模塊。 例如:

  • 1) 一次導入多個項目
// 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));

導入/導出模塊可使用別名

若是要避免命名衝突,能夠在導出和導入時使用別名。例如:

  • 1)重命名導出
// 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};
  • 2) 重命名導入
// main.js

import { add, multiply as mult } from './utils.js';

console.log(add(3, 7));
console.log(mult(3, 7));

你能夠查看 JavaScript 模塊簡史 和 ECMAScript 6 Modules(模塊)系統及語法詳解 來完整了解 JavaScript 模塊化進程和 ES6 Modules(模塊)的更多信息。

單例模式(Singleton Pattern)

Singleton(單例) 是一個只能實例化一次的對象。 若是不存在,則單例模式會建立類的新實例。 若是存在實例,則它只返回對該對象的引用。 對構造函數的任何重複調用老是會獲取相同的對象。

JavaScript 一直支持單例模式。 咱們只是不稱他們爲單例,咱們稱之爲 對象字面量。 例如:

const user = {
  name: 'Peter',
  age: 25,
  job: 'Teacher',
  greet: function() {
    console.log('Hello!');
  }
};

由於 JavaScript 中的每一個對象佔用一個惟一的內存位置,當咱們調用 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 對象是否存在。 若是該對象不存在,則將該變量分配給 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並,傳遞一個帶有 carType 屬性 options 對象,且值爲 car 或 truck 的來建立一個新的 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 對象做爲參數傳遞。 而後咱們覆蓋該對象的成本函數,該函數返回汽車的更新成本,並向該對象添加新屬性以指示添加了哪一個特徵。

要添加新功能,咱們能夠執行如下操做:

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

雖然瞭解各類設計模式很重要,但一樣重要的是不要過分使用它們。 在使用設計模式以前,你應該仔細考慮你所處的問題是否符合該設計模式。 要了解模式是否適合你的問題,你應該研究設計模式的思想,以及該設計模式的應用。

相關文章
相關標籤/搜索