深入理解JavaScript的設計模式

使用適當的設計模式可以幫助你編寫更好、更易於理解的代碼。這樣的代碼也更容易維護。但是,重要的是不要過度使用它們。在使用設計模式之前,你應該仔細考慮你的問題是否符合設計模式。

當你開始一個新的項目時,你不會立即開始編碼。你必須定義項目的目的和範圍,然後列出項目特性或規格說明。之後,你可以開始編寫代碼,或者,如果你正在參與的是一個更復雜的項目,那麼你應該選擇一個最適合項目的設計模式。

什麼是設計模式?

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

爲什麼要使用設計模式?

有許多程序員,他們要麼認爲設計模式浪費時間,要麼不知道如何恰當地應用它們。但是,使用適當的設計模式可以幫助你編寫更好、更易於理解的代碼。這樣的代碼也更容易維護。

最重要的是,設計模式爲軟件開發人員提供了一個可以談論的通用詞彙表。它們可以讓學習代碼的人快速瞭解代碼的意圖。

例如,如果你在項目中使用了裝飾模式,那麼新程序員就會立即知道那段代碼在做什麼,他們可以把更多的精力放在解決業務問題上,而不是試圖理解那段代碼在做什麼。

現在我們知道了什麼是設計模式,以及爲什麼它們很重要。接下來,讓我們深入探討下應用於 JavaScript 的各種設計模式。

模塊模式

模塊是一段自包含的代碼,因此,我們可以在不影響代碼其他部分的情況下更新模塊。模塊還允許我們通過爲變量創建單獨的作用域來避免命名空間污染。當模塊與其他代碼片段鬆耦合時,我們還可以在其他項目中重用它們。

模塊是任何現代化 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(),例如:

// 打印'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();
  }
  /** 把希望公開的方法和變量賦給對象屬性 */
return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();
myRevealingModule.setName('Mark');
// 打印姓名:Mark
myRevealingModule.getName();

這種模式使我們更容易理解哪些函數和變量可以公開訪問,這有助於提高代碼的可讀性。

執行代碼之後,myRevealingModule 是下面這個樣子:

const myRevealingModule = {
  setName: publicSetName,
  greeting: publicVar,
  getName: publicGetName
};

我們可以調用 myrevealingmodule. setname ('Mark'),它是對方法 publicSetName 的引用,而 myRevealingModule.getName() 是對內部方法 publicGetName 的引用,例如:

myRevealingModule.setName('Mark');
// 打印姓名: Mark
myRevealingModule.getName();

與模塊模式相比,揭示模塊模式的優點如下

  • 通過修改 return 語句中的一行代碼,我們就可以將成員從 public 更改爲 private,反之亦然。

  • 返回的對象不包含任何函數定義,所有右側表達式都在 iife 中定義,這使得代碼清晰且易於閱讀。

ES6 模塊

在 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;
}
// 這是一個私有函數
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;
}
// 這是一個私有函數
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

導入模塊
和輸出模塊類似,藉助 import,有多種方法可以導入模塊:

一次導入多個項:

// main.js
// 導入多個項
import { sum, multiply } from './utils.js';
console.log(sum(3, 7));
console.log(multiply(3, 7));

導入所有模塊:

// main.js
// 導入所有模塊
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));

單例模式

單例對象是隻能實例化一次的對象。如果一個類的實例不存在,單例模式就會創建一個新的類實例。如果實例存在,它只返回對該對象的引用。對構造函數的任何重複調用都會獲取相同的對象。

JavaScript 語言一直都內置了的單例,只是我們不把它們叫做單例,我們稱它們爲對象字面量,例如:

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

因爲 JavaScript 中的每個對象都佔用一個唯一的內存位置,當我們調用 user 對象時,我們本質上是返回了該對象的引用。

如果我們試圖將 user 變量複製到另一個變量中並修改該變量,例如:

const user1 = user;
user1.name = 'Mark';

我們會看到,兩個對象都被修改了,因爲在 JavaScript 中,對象是通過引用傳遞的,而不是值。因此,內存中只有一個對象,例如:

// 打印'Mark'
console.log(user.name);
// 打印'Mark'
console.log(user1.name);
// 打印 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();
// 打印 true
console.log(user1 === user2);

當調用這個構造函數時,它會檢查實例對象是否存在。如果對象不存在,它就將這個變量賦給實例變量。如果對象存在,它只返回那個對象。

單例也可以使用模塊模式實現,例如:

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();
// 打印 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 類(帶有一些默認值),用於創建新的 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'
});
// 打印 Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// 打印 Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

我已經創建了一個新的 VehicleFactory 類的對象工廠。之後,我們可以調用 factory.createVehicle 方法,傳入一個 vehicleType 屬性值爲 car 或 truck 的 options 對象。

裝飾模式

裝飾模式用於擴展對象的功能,而不修改現有的類或構造函數。該模式可用於向對象添加特性,而不修改使用它們的底層代碼。

下面是這個模式的一個簡單例子:

function Car(name) {
  this.name = name;
  // 默認值
  this.color = 'White';
}
// 新建一個需要裝飾的對象
const tesla= new Car('Tesla Model 3');
// 使用新功能裝飾對象
tesla.setColor = function(color) {
  this.color = color;
}
tesla.setPrice = function(price) {
  this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// 打印 black
console.log(tesla.color);

對於這種模式,一個更實際的例子是,比方說,一輛車的價格取決於它有多少功能。如果沒有裝飾模式,我們將不得不爲不同的特性組合創建不同的類,每個類都有計算成本的 cost 方法,例如:

class Car() {
}
class CarWithAC() {
}
class CarWithAutoTransmission {
}
class CarWithPowerLocks {
}
class CarWithACandPowerLocks {
}

但是使用裝飾模式,我們可以創建一個基類 Car,並使用裝飾函數將不同配置的成本添加到它的對象中,例如:

class Car {
  constructor() {
  // 默認值
  this.cost = function() {
  return 20000;
  }
}
}
// 裝飾函數
function carWithAC(car) {
  car.hasAC = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}
// 裝飾函數
function carWithAutoTransmission(car) {
  car.hasAutoTransmission = true;
   const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 2000;
  }
}
// 裝飾函數
function carWithPowerLocks(car) {
  car.hasPowerLocks = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}

首先,我們創建一個創建 Car 對象的基類 Car。然後,爲要添加的功能創建裝飾,並將 Car 對象作爲參數傳遞。然後,我們重寫這個對象的 cost 函數,該函數返回更新後的汽車成本,並向該對象添加一個新屬性,表明添加了哪些功能。

要添加新功能,我們可以這樣做:

const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);

最後,我們可以像下面這樣計算汽車的成本:

// 計算汽車的總成本
console.log(car.cost());

小結

我們已經瞭解了 JavaScript 中使用的各種設計模式,但還有一些可以用 JavaScript 實現的設計模式我在這裏沒有涉及。

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

 

本次給大家推薦一個免費的學習羣,裏面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。 對web開發技術感興趣的同學,歡迎加入Q羣:943129070,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視頻資料。 最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峯。