1. 程式人生 > >深入理解JavaScript的設計模式

深入理解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 實現的設計模式我在這裡沒有涉及。

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

原文連結:https://www.jianshu.com/p/17a...

原文地址:https://segmentfault.com/a/1190000017064683