前端設計模式大全(彙總詳細版)

2023-04-18 12:01:03

1. 工廠模式


工廠模式(Factory Pattern):將物件的建立和使用分離,由工廠類負責建立物件並返回。在前端開發中,可以使用工廠模式來動態建立元件。

前端中的工廠模式是一種建立物件的設計模式,它可以讓我們封裝建立物件的細節,我們使用工廠方法而不是直接呼叫 new 關鍵字來建立物件,使得程式碼更加清晰、簡潔和易於維護。在前端開發中,工廠模式通常用於建立多個相似但稍有不同的物件,比如建立一系列具有相同樣式和行為的按鈕或者表單。

在實現工廠模式時,通常需要建立一個工廠函數(或者叫做工廠類),該函數可以接受一些引數,並根據這些引數來建立物件。例如,我們可以建立一個ButtonFactory函數,它接受一個type引數,用於指定按鈕的型別,然後根據type引數建立不同型別的按鈕物件。範例程式碼如下:

function ButtonFactory(type) {
  switch (type) {
    case 'primary':
      return new PrimaryButton();
    case 'secondary':
      return new SecondaryButton();
    case 'link':
      return new LinkButton();
    default:
      throw new Error('Unknown button type: ' + type);
  }
}

function PrimaryButton() {
  this.type = 'primary';
  this.text = 'Click me!';
  this.onClick = function() {
    console.log('Primary button clicked!');
  };
}

function SecondaryButton() {
  this.type = 'secondary';
  this.text = 'Click me too!';
  this.onClick = function() {
    console.log('Secondary button clicked!');
  };
}

function LinkButton() {
  this.type = 'link';
  this.text = 'Click me as well!';
  this.onClick = function() {
    console.log('Link button clicked!');
  };
}

 

在上面的範例中,ButtonFactory函數接受一個type引數,根據這個引數來建立不同型別的按鈕物件。例如,如果type為primary,則返回一個PrimaryButton物件,該物件具有type、text和onClick屬性,表示一個主要按鈕。其他型別的按鈕也類似。

使用工廠模式可以讓我們將物件建立的過程與具體的業務邏輯分離開來,從而提高程式碼的可重用性和可維護性。

 

2. 單例模式


單例模式(Singleton Pattern):保證一個類只有一個範例,並提供一個存取它的全域性存取點。在前端開發中,可以使用單例模式來管理全域性狀態和資源。

在JavaScript中,單例模式可以通過多種方式實現,以下是一些常見的實現方式:

  1. 物件字面量

使用物件字面量可以輕鬆地建立單例物件,例如:

const singleton = {
  property1: "value1",
  property2: "value2",
  method1: function () {
    // ...
  },
  method2: function () {
    // ...
  },
};

 

上述程式碼中,使用了一個物件字面量來建立單例物件,該物件包含了一些屬性和方法。由於JavaScript中物件字面量本身就是單例的,因此不需要額外的程式碼來保證單例。

  1. 建構函式

在JavaScript中,每個建構函式都可以用於建立單例物件,例如:

function Singleton() {
  // 判斷是否存在範例
  if (typeof Singleton.instance === "object") {
    return Singleton.instance;
  }

  // 初始化單例物件
  this.property1 = "value1";
  this.property2 = "value2";
  Singleton.instance = this;
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // 輸出 true

 

上述程式碼中,使用了一個建構函式來建立單例物件。在建構函式中,首先判斷是否存在單例範例,如果存在則直接返回該範例,否則建立單例物件並將其儲存在 Singleton.instance 屬性中。由於JavaScript中每個建構函式本身就是一個單例,因此不需要額外的程式碼來保證單例。

  1. 模組模式

使用模組模式可以建立一個只有單個範例的物件,例如:

const Singleton = (function () {
  let instance;

  function init() {
    // 建立單例物件
    const object = new Object("I am the instance");
    return object;
  }

  return {
    getInstance: function () {
      if (!instance) {
        instance = init();
      }
      return instance;
    },
  };
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // 輸出 true

 

上述程式碼中,使用了一個立即執行函數來建立單例物件。在該函數中,定義了一個私有變數 instance 用於儲存單例範例,而 init 函數則是用於建立單例範例的方法。最後,返回一個物件,該物件包含一個 getInstance 方法,該方法用於獲取單例範例。

通過上述方式實現的單例模式,可以確保在程式執行期間,某個類只有一個範例,並且該範例可以在任何地方存取。

 

3. 釋出-訂閱模式


釋出-訂閱模式(Publish-Subscribe Pattern):也叫訊息佇列模式,它是一種將釋出者和訂閱者解耦的設計模式。在前端開發中,可以使用釋出-訂閱模式來實現元件之間的通訊。

JavaScript中的釋出/訂閱模式(Pub/Sub)是一種常用的設計模式。它允許在應用程式中定義物件之間的一對多的依賴關係,當一個物件的狀態發生變化時,所有依賴於它的物件都會被通知和更新。

在釋出/訂閱模式中,有兩種型別的物件:釋出者和訂閱者。釋出者是事件的發出者,它通常維護一個事件列表,並且可以向列表中新增或刪除事件。當某個事件發生時,它會將這個事件通知給所有訂閱者。訂閱者則是事件的接收者,它們訂閱感興趣的事件,並且在事件發生時接收通知。。

釋出訂閱模式可以幫助我們實現鬆耦合的設計,讓物件之間的依賴關係變得更加靈活。它在前端開發中的應用非常廣泛,例如 Vue.js 中的事件匯流排、Redux 中的 store 等。

以下是一個簡單的實現釋出/訂閱模式的範例程式碼:

// 定義一個釋出者物件
var publisher = {
  // 定義一個事件列表
  events: {},

  // 新增事件到列表中
  addEvent: function(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  // 從事件列表中刪除事件
  removeEvent: function(event, callback) {
    if (this.events[event]) {
      for (var i = 0; i < this.events[event].length; i++) {
        if (this.events[event][i] === callback) {
          this.events[event].splice(i, 1);
          break;
        }
      }
    }
  },

  // 釋出事件
  publishEvent: function(event, data) {
    if (this.events[event]) {
      for (var i = 0; i < this.events[event].length; i++) {
        this.events[event][i](data);
      }
    }
  }
};

// 定義一個訂閱者物件
var subscriber = {
  // 處理事件的回撥函數
  handleEvent: function(data) {
    console.log(data);
  }
};

// 訂閱一個事件
publisher.addEvent('event1', subscriber.handleEvent);

// 釋出一個事件
publisher.publishEvent('event1', 'Hello, world!');

// 取消訂閱一個事件
publisher.removeEvent('event1', subscriber.handleEvent);

 

在這個例子中,釋出者物件維護了一個事件列表(events),並且提供了新增、刪除和釋出事件的方法。訂閱者物件則提供了一個處理事件的回撥函數(handleEvent),它可以被新增到釋出者物件的事件列表中。當釋出者釋出一個事件時,所有訂閱了這個事件的訂閱者都會收到通知,並執行相應的處理常式。

 

4. 觀察者模式


觀察者模式(Observer Pattern):當物件間存在一對多的關係時,使用觀察者模式。當被觀察的物件發生變化時,其所有的觀察者都會收到通知並進行相應的操作。在JavaScript中,可以使用回撥函數或事件監聽來實現觀察者模式。

在前端開發中,觀察者模式常被用來實現元件間的資料傳遞和事件處理。比如,當一個元件的狀態發生改變時,可以通過觀察者模式來通知其他元件更新自身的狀態或檢視。

在觀察者模式中,通常會定義兩種角色:

  1. Subject(主題):它是被觀察的物件,當其狀態發生改變時會通知所有的觀察者。
  2. Observer(觀察者):它是觀察主題的物件,當主題狀態發生改變時會接收到通知並進行相應的處理。

以下是一個簡單的實現範例:

class Subject {
  constructor() {
    this.observers = []
  }
  
  addObserver(observer) {
    this.observers.push(observer)
  }
  
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer)
  }
  
  notify(data) {
    this.observers.forEach(obs => obs.update(data))
  }
}

class Observer {
  update(data) {
    console.log(`Received data: ${data}`)
  }
}

// Usage
const subject = new Subject()
const observer1 = new Observer()
const observer2 = new Observer()

subject.addObserver(observer1)
subject.addObserver(observer2)

subject.notify('Hello, world!')
// Output:
// Received data: Hello, world!
// Received data: Hello, world!

subject.removeObserver(observer1)

subject.notify('Goodbye, world!')
// Output:
// Received data: Goodbye, world!

 

在上面的範例中,我們定義了一個 Subject 類和一個 Observer 類。Subject 類有三個方法,addObserver 用於新增觀察者,removeObserver 用於移除觀察者,notify 用於通知所有觀察者。

Observer 類只有一個方法 update,用於接收主題傳遞的資料。我們建立了兩個 Observer 範例並將它們新增到了 Subject 範例中,然後呼叫了 notify 方法來通知它們更新資料。

在實際開發中,我們通常會使用現成的庫或框架來實現觀察者模式,比如 React 中的狀態管理庫 Redux 和事件處理庫 EventEmitter。

 

5. 中介者模式


中介者模式(Mediator Pattern):通過一箇中介物件來封裝一系列物件之間的互動。在JavaScript中,可以使用事件排程器來實現中介者模式。

在前端開發中,中介者模式常常被用於管理複雜的使用者介面或元件之間的互動,比如 GUI 元件、聊天室、遊戲等等。通過引入一箇中介者物件,各個元件可以向中介者物件傳送訊息或事件,而不需要知道訊息或事件的接收者是誰。中介者物件負責接收並分發訊息或事件,從而實現元件之間的解耦和統一管理。

下面是一個簡單的例子,展示瞭如何在前端中使用中介者模式:

// 中介者物件
const Mediator = {
  components: [],
  addComponent(component) {
    this.components.push(component);
  },
  broadcast(source, message) {
    this.components
      .filter(component => component !== source)
      .forEach(component => component.receive(message));
  }
};

// 元件物件
class Component {
  constructor() {
    this.mediator = Mediator;
    this.mediator.addComponent(this);
  }
  send(message) {
    this.mediator.broadcast(this, message);
  }
  receive(message) {
    console.log(`Received message: ${message}`);
  }
}

// 使用中介者模式進行元件之間的通訊
const componentA = new Component();
const componentB = new Component();
componentA.send("Hello from Component A");
componentB.send("Hi from Component B");

// Received message: Hello from Component A
// Received message: Hi from Component B

 

在上面的例子中,我們定義了一箇中介者物件 Mediator 和兩個元件物件 ComponentA 和 ComponentB。當元件物件傳送訊息時,它會將訊息傳送給中介者物件,中介者物件負責將訊息分發給其他元件物件。這樣,我們就實現了元件之間的解耦和統一管理。

需要注意的是,在實際開發中,我們可能需要使用不同的中介者物件來管理不同的元件之間的互動行為。此外,我們還可以使用其他方式來實現中介者模式,比如使用觀察者模式來實現。

 

6. 裝飾者模式


裝飾者模式(Decorator Pattern):動態地給一個物件新增額外的職責。在前端開發中,可以使用裝飾者模式來動態修改元件的行為和樣式。

JavaScript 中的裝飾者模式可以通過以下幾種方式實現:

  1. 通過擴充套件物件的屬性或方法來實現裝飾者模式
// 定義一個原始物件
const obj = {
  foo() {
    console.log('foo');
  }
}

// 定義一個裝飾函數,用於擴充套件原始物件的方法
function barDecorator(obj) {
  obj.bar = function() {
    console.log('bar');
  }
  return obj;
}

// 使用裝飾函數來擴充套件原始物件
const decoratedObj = barDecorator(obj);
decoratedObj.foo(); // 輸出 "foo"
decoratedObj.bar(); // 輸出 "bar"

 

在上面的範例中,我們首先定義了一個原始物件 obj,它包含一個方法 foo。然後,我們定義了一個裝飾函數 barDecorator,它接收一個物件作為引數,用於為這個物件新增一個新的方法 bar。最後,我們使用 barDecorator 函數來擴充套件原始物件 obj,並得到了一個新的物件 decoratedObj,它包含了原始物件的方法 foo 和新的方法 bar

  1. 通過擴充套件物件的原型來實現裝飾者模式
// 定義一個原始物件
function Foo() {}

// 在原型上定義一個方法
Foo.prototype.foo = function() {
  console.log('foo');
}

// 定義一個裝飾函數,用於擴充套件原型的方法
function barDecorator(clazz) {
  clazz.prototype.bar = function() {
    console.log('bar');
  }
}

// 使用裝飾函數來擴充套件原型
barDecorator(Foo);

// 建立一個新的物件,並使用擴充套件後的方法
const obj = new Foo();
obj.foo(); // 輸出 "foo"
obj.bar(); // 輸出 "bar"

 

在上面的範例中,我們首先定義了一個原始物件 Foo,它是一個建構函式,用於建立一個物件。然後,我們在原型上定義了一個方法 foo。接著,我們定義了一個裝飾函數 barDecorator,它接收一個建構函式作為引數,用於在原型上新增一個新的方法 bar。最後,我們使用 barDecorator 函數來擴充套件原始物件的原型,然後建立一個新的物件 obj,並使用擴充套件後的方法 foo 和 bar

需要注意的是,裝飾者模式可以巢狀使用,也就是說,我們可以通過多個裝飾函數來依次為一個元件新增多個不同的功能。同時,裝飾者模式也可以用於對已有的元件進行擴充套件,使得我們可以在不改變原有程式碼的情況下,給元件新增新的行為和樣式。

 

7. 策略模式


策略模式(Strategy Pattern):定義一系列的演演算法,將每一個演演算法都封裝起來,並且使它們可以相互替換。在前端開發中,可以使用策略模式來動態切換元件的演演算法和行為。

它可以讓我們在不改變物件本身的情況下,通過修改其內部的演演算法實現不同的行為。策略模式常常被用於實現一些複雜的業務邏輯,特別是需要根據不同的條件進行處理的情況。

下面是一個簡單的範例,演示瞭如何使用策略模式來實現一個計算器:

// 定義一個策略物件
const strategies = {
  add: function(num1, num2) {
    return num1 + num2;
  },
  subtract: function(num1, num2) {
    return num1 - num2;
  },
  multiply: function(num1, num2) {
    return num1 * num2;
  },
  divide: function(num1, num2) {
    return num1 / num2;
  }
};

// 定義一個計算器物件
const Calculator = function(strategy) {
  this.calculate = function(num1, num2) {
    return strategy(num1, num2);
  }
};

// 使用策略模式來建立一個計算器物件
const addCalculator = new Calculator(strategies.add);
const subtractCalculator = new Calculator(strategies.subtract);
const multiplyCalculator = new Calculator(strategies.multiply);
const divideCalculator = new Calculator(strategies.divide);

// 使用計算器物件進行計算
console.log(addCalculator.calculate(10, 5)); // 輸出 15
console.log(subtractCalculator.calculate(10, 5)); // 輸出 5
console.log(multiplyCalculator.calculate(10, 5)); // 輸出 50
console.log(divideCalculator.calculate(10, 5)); // 輸出 2

 

在上面的範例中,我們首先定義了一個策略物件,其中包含了四個不同的演演算法:加、減、乘和除。然後我們定義了一個計算器物件,它接收一個策略物件作為引數,並將其儲存在內部。最後,我們使用策略模式來建立四個不同的計算器物件,每個物件使用不同的演演算法進行計算。

這個範例展示瞭如何使用策略模式來實現一個簡單的計算器,但實際上它可以應用於許多其他的場景中,例如表單驗證、圖表繪製等。策略模式可以讓我們通過修改策略物件來改變物件的行為,從而實現更加靈活和可延伸的程式碼。

 

8. 介面卡模式


介面卡模式(Adapter Pattern):將一個類的介面轉化為使用者端所期望的介面,使得原本不相容的類可以一起工作。在前端開發中,可以使用介面卡模式來處理不同瀏覽器之間的相容性問題。

介面卡模式通常包含三個角色:使用者端、目標物件和介面卡物件。使用者端呼叫介面卡物件的介面,介面卡物件再呼叫目標物件的介面,將目標物件的介面轉換為使用者端需要的介面,從而實現相容性。

另外,介面卡模式也可以用於將不同的第三方元件或外掛進行整合和相容。例如,當一個網站需要使用不同的圖表庫來繪製圖表時,可以使用介面卡模式將這些圖表庫進行封裝,從而實現統一的呼叫介面,方便使用和維護。

下面是一個簡單的例子,演示如何使用介面卡模式將不同的 API 介面進行統一封裝:

// 目標介面
class Target {
  request() {
    return 'Target: 請求完成!';
  }
}

// 需要適配的物件
class Adaptee {
  specificRequest() {
    return 'Adaptee: 請求完成!';
  }
}

// 介面卡物件
class Adapter extends Target {
  constructor(adaptee) {
    super();
    this.adaptee = adaptee;
  }

  request() {
    const result = this.adaptee.specificRequest();
    return `Adapter: ${result}`;
  }
}

// 使用介面卡模式
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // 輸出:Adapter: Adaptee: 請求完成!

 


在上面的程式碼中,我們定義了一個目標介面 Target 和一個需要適配的物件 Adaptee,它們之間的介面不相容。然後我們使用介面卡模式,將 Adaptee 物件適配為 Target 介面,從而實現了相容性。

介面卡物件 Adapter 繼承了目標介面 Target,並在其內部使用了需要適配的物件 Adaptee。在 Adapter 的 request 方法中,我們呼叫了 Adaptee 的 specificRequest 方法,將其返回值包裝為符合 Target 介面的形式。

通過介面卡模式,我們可以將不同介面的物件進行統一封裝,從而方便我們使用和維護程式碼。

 

9. 職責鏈模式


職責鏈模式(Chain of Responsibility Pattern):為了避免請求傳送者與多個請求處理者耦合在一起,將所有請求的處理者通過前一物件記住其下一個物件的方式連成一條鏈,然後請求沿著鏈傳遞,直到有一個物件處理它為止。在JavaScript中,可以使用函數物件或物件字面量來實現職責鏈模式。

職責鏈模式通常涉及一系列處理物件,每個物件都負責處理請求的一部分,並將請求傳遞給下一個物件,直到請求得到滿足或者處理結束。這種方式可以將系統中的不同操作解耦,從而提高系統的靈活性和可維護性。

在 JavaScript 中,職責鏈模式的實現通常涉及使用一個處理物件的連結串列,其中每個物件都有一個指向下一個物件的參照。當請求進入系統時,它首先被傳遞給連結串列中的第一個物件。如果這個物件不能處理請求,則將請求傳遞給連結串列中的下一個物件,直到找到能夠處理請求的物件為止。

下面是一個簡單的 JavaScript 職責鏈模式的範例:

class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNextHandler(handler) {
    this.nextHandler = handler;
  }

  handleRequest(request) {
    if (this.nextHandler) {
      this.nextHandler.handleRequest(request);
    }
  }
}

class ConcreteHandler1 extends Handler {
  handleRequest(request) {
    if (request === 'request1') {
      console.log('ConcreteHandler1 handles the request');
    } else {
      super.handleRequest(request);
    }
  }
}

class ConcreteHandler2 extends Handler {
  handleRequest(request) {
    if (request === 'request2') {
      console.log('ConcreteHandler2 handles the request');
    } else {
      super.handleRequest(request);
    }
  }
}

const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();

handler1.setNextHandler(handler2);

handler1.handleRequest('request1'); // Output: "ConcreteHandler1 handles the request"
handler1.handleRequest('request2'); // Output: "ConcreteHandler2 handles the request"
handler1.handleRequest('request3'); // Output: Nothing is printed

在上面的範例中,Handler 類是職責鏈模式的基礎類別,它包含一個指向下一個處理物件的參照。ConcreteHandler1 和 ConcreteHandler2 類是具體的處理物件,它們根據請求的型別來決定是否能夠處理請求。如果不能處理,則將請求傳遞給下一個處理物件。最後,我們將 handler1 物件的下一個處理物件設定為 handler2 物件,然後依次呼叫 handleRequest 方法來模擬不同型別的請求。

 

10. 代理模式


代理模式(Proxy Pattern):前端設計模式中的代理模式是一種結構型模式,它允許在不改變原始物件的情況下,通過引入一個代理物件來控制對原始物件的存取。代理物件充當原始物件的中介,使用者端與代理物件互動,代理物件再將請求轉發給原始物件。

代理模式在前端開發中經常被用來處理一些複雜或者耗時的操作,例如圖片的懶載入、快取等。代理物件可以在載入圖片時顯示預留位置,當圖片載入完成後再替換預留位置,從而提高頁面載入速度和使用者體驗。

另外,代理模式還可以用來實現一些許可權控制的功能。例如,在使用者登入後,代理物件可以檢查使用者的許可權,只有具有相應許可權的使用者才能夠存取某些功能或者頁面。

在 JavaScript 中,代理模式通常使用 ES6 中新增的 Proxy 物件來實現。Proxy 物件允許攔截對物件的各種操作,包括讀取、賦值、函數呼叫等。通過使用 Proxy 物件,我們可以在不改變原始物件的情況下,控制對原始物件的存取。

當我們需要為某個類或者物件新增一些額外的行為或者控制存取時,可以使用代理模式。下面是一個簡單的範例,使用代理模式實現圖片懶載入的功能。

// 原始物件 - 圖片
class Image {
  constructor(url) {
    this.url = url;
  }

  // 載入圖片
  load() {
    console.log(`Image loaded: ${this.url}`);
  }
}

// 代理物件 - 圖片
class ProxyImage {
  constructor(url) {
    this.url = url;
    this.image = null; // 延遲載入
  }

  // 載入圖片
  load() {
    if (!this.image) {
      this.image = new Image(this.url); // 延遲載入圖片
      console.log(`Placeholder loaded for ${this.url}`);
    }
    this.image.load(); // 顯示圖片
  }
}

// 使用者端程式碼
const img1 = new ProxyImage('https://example.com/image1.jpg');
const img2 = new ProxyImage('https://example.com/image2.jpg');

img1.load(); // Placeholder loaded for https://example.com/image1.jpg, Image loaded: https://example.com/image1.jpg
img1.load(); // Image loaded: https://example.com/image1.jpg

img2.load(); // Placeholder loaded for https://example.com/image2.jpg, Image loaded: https://example.com/image2.jpg

在上面的範例中,原始物件是 Image 類,代理物件是 ProxyImage 類。當用戶端程式碼呼叫 load() 方法時,代理物件會首先載入預留位置,並延遲載入圖片。如果圖片已經被載入過了,代理物件會直接顯示圖片,否則代理物件會載入圖片並顯示。通過使用代理模式,我們可以在不影響原始物件的情況下,實現了圖片的懶載入功能,提高了頁面載入速度和使用者體驗。

 

11. 命令模式


命令模式(Command Pattern):它允許你將操作封裝成物件。這些物件包括了被呼叫的方法及其引數。這些命令物件可以被儲存、傳遞和執行。

在前端開發中,命令模式可以被用於實現可復原和重做的操作。例如,在一個文字編輯器中,可以使用命令模式來實現復原和重做操作。對於每一個編輯操作,可以建立一個命令物件來表示這個操作,然後將這個命令物件儲存在一個歷史列表中。當需要復原操作時,可以從歷史列表中取出最近的命令物件並執行它的反向操作。

命令模式還可以被用於實現選單和工具列等使用者介面元素。例如,可以建立一個選單項物件來表示一個命令,並將這個物件新增到選單中。當用戶點選這個選單項時,選單項物件將執行對應的操作。

下面是一個簡單的實現可復原操作的例子:

// 定義一個命令物件
class Command {
  constructor(receiver, args) {
    this.receiver = receiver;
    this.args = args;
    this.executed = false;
  }
  
  execute() {
    if (!this.executed) {
      this.receiver.execute(this.args);
      this.executed = true;
    }
  }
  
  undo() {
    if (this.executed) {
      this.receiver.undo(this.args);
      this.executed = false;
    }
  }
}

// 定義一個接收者物件
class Receiver {
  constructor() {
    this.value = 0;
  }
  
  execute(args) {
    this.value += args;
    console.log(`執行操作,value = ${this.value}`);
  }
  
  undo(args) {
    this.value -= args;
    console.log(`復原操作,value = ${this.value}`);
  }
}

// 建立一個接收者物件和一些命令物件
const receiver = new Receiver();
const command1 = new Command(receiver, 1);
const command2 = new Command(receiver, 2);
const command3 = new Command(receiver, 3);

// 建立一個歷史列表並將命令物件新增到其中
const history = [command1, command2, command3];

// 依次執行命令物件
history.forEach((command) => {
  command.execute();
});

// 復原最後一個操作
history.pop().undo(); // 復原操作,value = 3

在這個例子中,Command 類表示一個命令物件,它包含了一個接收者物件、命令引數以及一個 executed 屬性,用於標記命令是否已經被執行過。Receiver 類表示接收者物件,它實現了具體的操作。在這個例子中,命令物件執行的操作是將 args 引數加到 Receiver 物件的 value 屬性上。命令物件的 execute 和 undo 方法分別執行和復原這個操作,並通過 executed 屬性來避免重複執行操作。

在實際的前端開發中,命令模式的應用還有很多,比如實現動畫效果的控制器、網路請求的佇列等。命令模式可以讓程式碼更加靈活和可延伸,同時也可以更好地實現程式碼的解耦。

 

12. 迭代器模式


迭代器模式(Iterator Pattern):提供一種方法順序存取一個聚合物件中的各個元素,而不需要暴露該物件的內部表示。在JavaScript中,可以使用迭代器模式來運算元組或類陣列物件。

在迭代器模式中,集合物件包含一個方法,用於返回一個迭代器,該迭代器可以按順序存取該集合中的元素。迭代器提供了一種通用的介面,使得可以使用相同的方式遍歷不同型別的集合物件。

在前端開發中,迭代器模式經常用於處理集合資料,例如陣列、列表等。通過使用迭代器模式,可以輕鬆地遍歷集合物件的元素,而不必擔心它們的實現方式。

以下是一個使用迭代器模式的範例:

// 定義一個集合類
class Collection {
  constructor() {
    this.items = [];
  }
  add(item) {
    this.items.push(item);
  }
  [Symbol.iterator]() {
    let index = 0;
    const items = this.items;
    return {
      next() {
        if (index < items.length) {
          return { value: items[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

// 建立一個集合物件
const collection = new Collection();
collection.add('item 1');
collection.add('item 2');
collection.add('item 3');

// 使用迭代器遍歷集合物件
const iterator = collection[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}

// item 1
// item 2
// item 3
// {done: true}

在上面的範例中,定義了一個名為 Collection 的集合類,該類包含一個 add 方法,用於向集合中新增元素。該類還實現了一個名為 [Symbol.iterator] 的特殊方法,用於返回一個迭代器物件。迭代器物件包含一個 next 方法,用於返回集合中的下一個元素,直到集合的所有元素都被遍歷完畢。

通過使用迭代器模式,我們可以輕鬆地遍歷集合物件的元素,而不必擔心它們的實現方式。

 

13. 組合模式


組合模式(Composite Pattern):它允許將物件組合成樹形結構,並且可以像操作單個物件一樣操作整個樹形結構。

組合模式(Composite Pattern)是一種結構型設計模式,它允許將物件組合成樹形結構,並且可以像操作單個物件一樣操作整個樹形結構。

組合模式的核心思想是將物件組織成樹形結構,其中包含組合物件和葉子物件兩種型別。組合物件可以包含葉子物件或其他組合物件,從而形成一個樹形結構。

組合模式可以應用於以下場景:

  1. UI元件庫:例如在一個複雜的UI元件庫中,一個複雜的元件可以由多個子元件組成,而每個子元件又可以由更小的元件組成。這種情況下,可以使用組合模式將每個元件看作一個節點,從而構建一個樹形結構。
  2. 樹形結構資料的處理:例如在一個檔案管理器中,資料夾和檔案可以看作是組合物件和葉子物件。通過組合模式,可以輕鬆地處理資料夾和檔案的層級關係,同時可以對整個資料夾進行操作,比如複製、貼上和刪除等。

實現組合模式通常有兩種方式:

  1. 使用類繼承:通過定義一個抽象的 Component 類和兩個具體的 Composite 和 Leaf 類來實現。Composite 類繼承自 Component 類,並且擁有一個子節點列表。Leaf 類繼承自 Component 類,並且沒有子節點。這種方式的實現比較傳統,但是需要使用類繼承,可能會導致類層次結構比較複雜。
  2. 使用物件組合:通過使用物件字面量和原型繼承等技術來實現。這種方式可以不需要類繼承,而是使用物件字面量和原型鏈來模擬組合模式的結構,比較靈活,但是程式碼可能比較冗長。

下面是一個使用物件字面量和原型繼承的組合模式實現範例:

// Component
const Component = {
  add: function () {},
  remove: function () {},
  getChild: function () {},
};

// Composite
function createComposite() {
  const composite = Object.create(Component);
  composite.children = [];

  composite.add = function (child) {
    this.children.push(child);
  };

  composite.remove = function (child) {
    const index = this.children.indexOf(child);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  };

  composite.getChild = function (index) {
    return this.children[index];
  };

  return composite;
}

// Leaf
function createLeaf() {
  const leaf = Object.create(Component);

  // Leaf 類實現自己的 add、remove、getChild 方法

  return leaf;
}

// 使用範例
const root = createComposite();
const branch1 = createComposite();
const branch2 = createComposite();
const leaf1 = createLeaf();
const leaf2 = createLeaf();
const leaf3 = createLeaf();

root.add(branch1);
root.add(branch2);
branch1.add(leaf1);
branch2.add(leaf2);
branch2.add(leaf3);

console.log(root); // 輸出樹形結構

上述範例中,通過使用物件字面量和原型繼承,模擬了組合模式的結構,從而實現了樹形結構的物件。在實際應用中,根據具體的需求和程式碼架構,可以選擇適合自己的實現方式。

 

14. 原型模式


原型模式(Prototype Pattern):使用原型範例指定建立物件的種類,並通過拷貝這些原型建立新的物件。

在JavaScript中,所有的物件都有一個原型鏈。原型鏈是一種機制,它允許我們在物件上定義屬性和方法,並且可以從它的原型中繼承屬性和方法。當我們存取一個物件的屬性或方法時,JavaScript會在原物件上查詢,如果找不到,它會繼續查詢原型鏈上的物件,直到找到該屬性或方法或者到達原型鏈的末端。

原型模式是一種利用原型鏈的設計模式,它允許我們通過克隆現有物件來建立新物件。JavaScript中的原型模式使用Object.create()方法來建立一個物件,並且可以通過修改原型鏈上的屬性和方法來修改新物件的行為。

使用原型模式的主要優點是它可以減少物件建立的時間和成本。它避免了在建立物件時需要執行許多計算或呼叫其他物件的建構函式的開銷。相反,它使用現有物件作為基礎,並通過克隆來建立新物件,從而提高了效能和效率。

下面是一個使用原型模式建立表單物件的範例程式碼:

// 定義一個表單物件的原型
var formPrototype = {
  fields: [],
  addField: function(field) {
    this.fields.push(field);
  },
  getFields: function() {
    return this.fields;
  },
  clone: function() {
    // 克隆表單物件並返回新物件
    var newForm = Object.create(this);
    newForm.fields = Object.create(this.fields);
    return newForm;
  }
};

// 建立一個表單物件
var form = Object.create(formPrototype);

// 新增表單欄位
form.addField('name');
form.addField('email');
form.addField('phone');

// 克隆表單物件並修改其中的欄位
var newForm = form.clone();
newForm.addField('address');

// 列印表單物件和新表單物件的欄位
console.log(form.getFields()); // ["name", "email", "phone"]
console.log(newForm.getFields()); // ["name", "email", "phone", "address"]

在這個範例中,我們首先定義了一個表單物件的原型,它包含一個空的欄位陣列、新增欄位和獲取欄位的方法以及一個克隆方法。然後,我們建立了一個表單物件,並向其新增了三個欄位。接下來,我們使用原型模式克隆表單物件並新增一個新欄位。最後,我們列印了表單物件和新表單物件的欄位,以驗證克隆方法是否正常工作。

 

15. 橋接模式


橋接模式(Bridge Pattern):用於將一個大類或一系列緊密相關的類拆分為抽象和實現兩個獨立的層次結構,從而能夠更好地組合和擴充套件這些類。

在前端開發中,橋接模式通常用於處理 UI 元件的複雜性,將元件的抽象與實現分離,使得它們能夠獨立地變化。通過橋接模式,我們可以讓元件的行為和樣式分別獨立變化,從而避免在程式碼中出現過多的重複和複雜度。

具體來說,橋接模式包含兩個關鍵部分:

  • 抽象部分(Abstraction):定義了元件的抽象介面和行為,它依賴於一個實現部分的物件。
  • 實現部分(Implementation):定義了元件的實現介面和樣式,它被抽象部分所依賴。

通過將抽象部分與實現部分解耦,我們可以在不影響原有程式碼的情況下,方便地擴充套件和修改元件的行為和樣式。同時,橋接模式也可以提高程式碼的可讀性和可維護性,使得程式碼更加清晰、簡潔和易於維護。

以下是一個簡單的前端橋接模式範例,假設我們需要實現一個 UI 元件庫,其中包含多種樣式的按鈕。

首先,我們建立一個抽象部分(Button)和兩個實現部分(DefaultButton 和 OutlineButton),它們分別定義了按鈕的抽象介面和樣式,然後,我們可以建立不同型別的按鈕,並將其與不同樣式的按鈕相結合:

// 抽象部分
class Button {
  constructor(implementation) {
    this.implementation = implementation;
  }

  click() {
    this.implementation.onClick();
  }

  render() {
    return this.implementation.render();
  }
}

// 實現部分 - 預設樣式
class DefaultButton {
  onClick() {
    console.log("DefaultButton clicked");
  }

  render() {
    return "<button class='default'>Default Button</button>";
  }
}

// 實現部分 - 輪廓樣式
class OutlineButton {
  onClick() {
    console.log("OutlineButton clicked");
  }

  render() {
    return "<button class='outline'>Outline Button</button>";
  }
}

// 建立不同型別的按鈕
const primaryButton = new Button(new DefaultButton());
const secondaryButton = new Button(new OutlineButton());

// 渲染並繫結按鈕事件
document.body.innerHTML = `
  ${primaryButton.render()}
  ${secondaryButton.render()}
`;

document.querySelector(".default").addEventListener("click", () => {
  primaryButton.click();
});

document.querySelector(".outline").addEventListener("click", () => {
  secondaryButton.click();
});

最後,當用戶點選按鈕時,會觸發相應的行為,同時也會根據按鈕的樣式渲染出不同的外觀效果。

這是一個非常簡單的範例,但是它展示瞭如何使用橋接模式來處理 UI 元件的複雜性,通過將抽象和實現分離,可以方便地擴充套件和修改元件的行為和樣式,從而提高程式碼的可維護性和可讀性。

 

16. 狀態模式


狀態模式(State Pattern):將物件的行為和狀態分離,使得物件可以根據不同的狀態來改變自己的行為。在前端開發中,可以使用狀態模式來管理頁面的狀態和響應使用者的互動。

在狀態模式中,物件的行為取決於其內部狀態,當狀態發生變化時,物件的行為也會相應地發生改變。這種模式通過將狀態抽象為獨立的類來實現,每個狀態類都有其自己的行為和相應的方法。

在前端開發中,狀態模式通常用於處理複雜的使用者互動邏輯,例如根據使用者的操作更改頁面上的狀態。以下是狀態模式的一些常見應用場景:

  1. 表單驗證:在使用者提交表單之前,可以使用狀態模式來驗證表單中的輸入是否符合規定。
  2. 遊戲開發:在遊戲中,物件的狀態可能會隨著遊戲的程序而發生變化。使用狀態模式可以幫助開發者輕鬆管理和處理這些狀態變化。
  3. 狀態切換:在前端應用中,一些操作會導致頁面狀態的改變,例如開啟或關閉側邊欄,展開或摺疊列表等。使用狀態模式可以幫助開發者管理這些狀態變化。

總之,狀態模式是一種靈活而有用的設計模式,可以使程式碼更加清晰和可維護,它可以幫助開發者輕鬆管理和處理複雜的使用者互動邏輯。

以下是一個簡單的狀態模式範例,假設我們正在開發一個購物車頁面,使用者可以向購物車中新增或刪除商品。購物車的狀態可以是「空的」或「非空的」,當購物車為空時,使用者需要被提示新增商品,當購物車非空時,使用者可以檢視商品列表和刪除商品。

首先,我們定義一個購物車狀態的抽象類:

class CartState {
  addToCart() {}
  removeFromCart() {}
  showMessage() {}
}

接下來,我們定義購物車的兩種狀態:空狀態和非空狀態。空狀態下使用者需要被提示新增商品,非空狀態下使用者可以檢視商品列表和刪除商品。

class EmptyCartState extends CartState {
  addToCart() {
    console.log('Product added to cart');
    // 將購物車狀態設定為非空狀態
    this.cart.setState(new NonEmptyCartState(this.cart));
  }

  showMessage() {
    console.log('Your cart is empty, please add some products.');
  }
}

class NonEmptyCartState extends CartState {
  removeFromCart() {
    console.log('Product removed from cart');
    // 如果商品列表為空,則將購物車狀態設定為空狀態
    if (this.cart.products.length === 0) {
      this.cart.setState(new EmptyCartState(this.cart));
    }
  }

  showMessage() {
    console.log(`Your cart contains ${this.cart.products.length} products:`);
    console.log(this.cart.products.join(', '));
  }
}

最後,我們定義購物車類,並在購物車類中使用狀態模式:

class ShoppingCart {
  constructor() {
    // 初始狀態為購物車為空
    this.state = new EmptyCartState(this);
    this.products = [];
  }

  setState(state) {
    this.state = state;
  }

  addToCart(product) {
    this.products.push(product);
    this.state.addToCart();
  }

  removeFromCart(product) {
    const index = this.products.indexOf(product);
    if (index !== -1) {
      this.products.splice(index, 1);
      this.state.removeFromCart();
    }
  }

  showMessage() {
    this.state.showMessage();
  }
}

在上面的程式碼中,我們建立了一個ShoppingCart類,它包含了兩個主要方法addToCartremoveFromCart,這些方法將商品新增到購物車中或從購物車中刪除。當這些方法被呼叫時,購物車的狀態將會根據商品列表的狀態自動更新。另外,我們還提供了一個showMessage方法,用於顯示購物車當前的狀態資訊。

現在,我們可以使用這個購物車類來管理我們的購物車狀態了:

const cart = new ShoppingCart();

cart.showMessage(); // Your cart is empty, please add some products.

cart.addToCart('apple');
cart.addToCart('banana');

cart.showMessage(); // Your cart contains 2 products: apple, banana.

cart.removeFromCart('apple');

cart.showMessage(); // Your cart contains 1 products: banana.

cart.removeFromCart('banana');

cart.showMessage(); // Your cart is empty, please add some products.

通過使用狀態模式,我們可以輕鬆管理購物車的狀態,並且程式碼更加清晰和可維護。

 

17. 模板方法模式


模板方法模式(Template Method Pattern):定義一個行為的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個行為的結構即可重定義該行為的某些特定步驟。

這些步驟被稱為「具體操作」(Concrete Operations),而整個行為的結構和順序則被稱為「模板方法」(Template Method)。

模板方法模式的核心思想是封裝行為中的不變部分,同時允許可變部分通過子類來進行擴充套件。這樣做的好處是可以避免重複程式碼,提高程式碼的複用性和可維護性。

在前端開發中,模板方法模式通常用於處理頁面的渲染和事件處理。例如,我們可以定義一個基礎的頁面渲染演演算法,並在其中定義一些抽象方法,如初始化資料、繫結事件、渲染模板等,然後在子類中實現這些具體操作。這樣可以使得我們在開發頁面時,只需要關注具體的業務邏輯,而不用過多關注頁面的渲染細節。

下面是一個簡單的模板方法模式的範例程式碼:

class Algorithm {
  templateMethod() {
    this.stepOne();
    this.stepTwo();
    this.stepThree();
  }

  stepOne() {
    throw new Error("Abstract method 'stepOne' must be implemented in subclass.");
  }

  stepTwo() {
    throw new Error("Abstract method 'stepTwo' must be implemented in subclass.");
  }

  stepThree() {
    throw new Error("Abstract method 'stepThree' must be implemented in subclass.");
  }
}

class ConcreteAlgorithm extends Algorithm {
  stepOne() {
    console.log('ConcreteAlgorithm: step one.');
  }

  stepTwo() {
    console.log('ConcreteAlgorithm: step two.');
  }

  stepThree() {
    console.log('ConcreteAlgorithm: step three.');
  }
}

const algorithm = new ConcreteAlgorithm();
algorithm.templateMethod();

在這個範例中,我們定義了一個 Algorithm 類,其中包含了一個模板方法 templateMethod() 和三個基本方法 stepOne()stepTwo() 和 stepThree()。這些基本方法都是抽象方法,需要在子類中進行實現。

我們還定義了一個 ConcreteAlgorithm 類,它繼承自 Algorithm 類,並實現了父類別中的三個基本方法。然後,我們建立了一個 ConcreteAlgorithm 的範例,並呼叫了其 templateMethod() 方法,該方法會按照父類別定義的順序執行三個基本方法。

總的來說,模板方法模式是一種非常實用的設計模式,在 JavaScript 中也同樣適用。它可以幫助我們將程式碼的結構和行為進行分離,從而提高程式碼的可讀性和可維護性。

 

18. 過濾器模式


過濾器模式(Filter Pattern):定義一個過濾器函數,該函數可以接受一個資料集合和一個過濾條件,返回符合條件的資料集合。

在過濾器模式中,我們有一個包含多個物件的列表,需要根據一些條件來篩選出符合條件的物件。通常情況下,可以使用多個過濾器來實現這個功能。每個過濾器都是一個獨立的類,它實現了一個過濾條件,我們可以將這些過濾器組合在一起,來實現複雜的過濾操作。

在實際開發中,可以使用過濾器模式來實現諸如搜尋、過濾、排序等功能。例如,在一個商品列表頁面中,我們可以使用過濾器模式來根據價格、品牌、型別等條件來篩選出使用者感興趣的商品。

以下是一個簡單的 JavaScript 範例程式碼,演示瞭如何使用過濾器模式來篩選陣列中的元素:

class Filter {
  constructor(criteria) {
    this.criteria = criteria;
  }

  meetCriteria(elements) {
    return elements.filter(element => this.criteria(element));
  }
}

class PriceFilter extends Filter {
  constructor(price) {
    super(element => element.price <= price);
  }
}

class BrandFilter extends Filter {
  constructor(brand) {
    super(element => element.brand === brand);
  }
}

const products = [
  { name: 'Product A', price: 10, brand: 'Brand A' },
  { name: 'Product B', price: 20, brand: 'Brand B' },
  { name: 'Product C', price: 30, brand: 'Brand C' },
];

const priceFilter = new PriceFilter(20);
const brandFilter = new BrandFilter('Brand A');

const filteredProducts = priceFilter.meetCriteria(products);
const finalProducts = brandFilter.meetCriteria(filteredProducts);

console.log(finalProducts);
// Output: [{ name: 'Product A', price: 10, brand: 'Brand A' }]

在上面的範例程式碼中,我們定義了一個 Filter 類作為過濾器模式的基礎類別,然後我們定義了兩個子類 PriceFilter 和 BrandFilter,它們分別根據價格和品牌來過濾商品。我們還定義了一個商品陣列 products,然後使用這兩個過濾器來篩選出價格低於等於 20 並且品牌為 'Brand A' 的商品,最後列印出符合條件的商品列表。

 

19. 備忘錄模式


備忘錄模式(Memento Pattern):是一種行為型設計模式,在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。在JavaScript中,可以使用閉包來實現備忘錄模式。

在前端設計中,備忘錄模式通常用於處理使用者介面的狀態。當用戶與應用程式互動時,應用程式會根據使用者的輸入更改其狀態。它可以使您儲存和還原應用程式狀態的快照,以便使用者可以隨時返回以前的狀態。

以下是備忘錄模式的幾個關鍵元件:

  1. Originator(發起人):負責建立要儲存狀態的物件,並在需要時將其狀態儲存到 Memento 中。
  2. Memento(備忘錄):儲存發起人物件的狀態。
  3. Caretaker(管理者):負責儲存和管理 Memento 物件。Caretaker 物件不會直接存取 Memento 物件,而是通過發起人物件來獲取它。

在前端中,備忘錄模式的一個實際應用是瀏覽器歷史記錄。當用戶在瀏覽器中導航時,瀏覽器將當前頁面的狀態儲存到歷史記錄中。使用者可以隨時返回以前的狀態,並恢復頁面的先前狀態。

在 JavaScript 中,備忘錄模式的實現和其他程式語言非常相似。下面是一個簡單的 JavaScript 實現範例:

// Originator
class Editor {
  constructor() {
    this.content = '';
  }

  type(words) {
    this.content += words;
  }

  save() {
    return new EditorMemento(this.content);
  }

  restore(memento) {
    this.content = memento.getContent();
  }
}

// Memento
class EditorMemento {
  constructor(content) {
    this.content = content;
  }

  getContent() {
    return this.content;
  }
}

// Caretaker
class History {
  constructor() {
    this.states = [];
  }

  push(state) {
    this.states.push(state);
  }

  pop() {
    return this.states.pop();
  }
}

// Usage
const editor = new Editor();
const history = new History();

// 編輯器輸入內容
editor.type('Hello, ');
editor.type('World!');

// 將當前狀態儲存到備忘錄中,並將備忘錄新增到歷史記錄中
history.push(editor.save());

// 繼續編輯器輸入內容
editor.type(' How are you today?');

// 輸出當前編輯器內容
console.log(editor.content); // 'Hello, World! How are you today?'

// 從歷史記錄中恢復上一個狀態
editor.restore(history.pop());

// 輸出恢復的編輯器內容
console.log(editor.content); // 'Hello, World!'

在這個例子中,Editor 類是發起人,它具有儲存和恢復狀態的方法。EditorMemento 類是備忘錄,它儲存發起人物件的狀態。History 類是管理者,它儲存和管理備忘錄物件。

在使用備忘錄模式時,我們首先建立一個編輯器物件 editor,並將其狀態儲存到歷史記錄中。然後我們繼續輸入一些內容,並再次將狀態儲存到歷史記錄中。接著,我們從歷史記錄中恢復先前的狀態,並輸出恢復的編輯器內容。

需要注意的是,在 JavaScript 中,我們可以直接存取物件的屬性,而不需要使用 getter 和 setter 方法,因此在上面的範例中,我們可以直接使用 editor.content 來存取編輯器的內容。

 

20. 外觀模式


外觀模式(Facade Pattern):它提供了一個簡單的介面,用於存取複雜的系統或子系統。通過外觀模式,使用者端可以通過一個簡單的介面來存取複雜的系統,而無需瞭解系統內部的具體實現細節。

在前端開發中,外觀模式常常被用於封裝一些常用的操作,以簡化程式碼複雜度和提高程式碼可維護性。比如,一個用於處理資料的模組可能包含很多複雜的程式碼邏輯和 API 呼叫,但是我們可以使用外觀模式將這些複雜的操作封裝到一個簡單的介面中,讓其他部分的程式碼可以通過這個介面來運算元據,而無需關心具體的實現細節。

外觀模式的優點在於它可以將系統的複雜性隱藏起來,從而降低程式碼的複雜度和耦合度。同時,外觀模式也可以提高程式碼的可讀性和可維護性,因為它可以將一些常用的操作封裝到一個統一的介面中,讓程式碼更加清晰易懂。

下面是一個外觀模式的範例程式碼:

// 複雜的系統或子系統
const moduleA = {
  method1: () => {
    console.log('method1 from moduleA');
  },
  method2: () => {
    console.log('method2 from moduleA');
  },
  method3: () => {
    console.log('method3 from moduleA');
  }
};

const moduleB = {
  method4: () => {
    console.log('method4 from moduleB');
  },
  method5: () => {
    console.log('method5 from moduleB');
  },
  method6: () => {
    console.log('method6 from moduleB');
  }
};

// 外觀物件,封裝了底層的操作,提供了一個簡單的介面
const facade = {
  method1: () => {
    moduleA.method1();
  },
  method2: () => {
    moduleA.method2();
  },
  method3: () => {
    moduleA.method3();
  },
  method4: () => {
    moduleB.method4();
  },
  method5: () => {
    moduleB.method5();
  },
  method6: () => {
    moduleB.method6();
  }
};

// 使用者端呼叫外觀物件的方法
facade.method1(); // 輸出:method1 from moduleA
facade.method2(); // 輸出:method2 from moduleA
facade.method4(); // 輸出:method4 from moduleB
facade.method6(); // 輸出:method6 from moduleB

在這個例子中,我們定義了兩個模組 moduleA 和 moduleB,它們都包含了一些方法。然後,我們定義了一個名為 facade 的外觀物件,它包含了這兩個模組的所有方法,並提供了一個簡單的介面,讓使用者端可以直接呼叫這些方法。最後,我們在使用者端呼叫外觀物件的方法,實際上是間接呼叫了底層模組的方法。

需要注意的是,外觀模式並不是一種萬能的設計模式,它並不能解決所有的問題。在某些情況下,使用外觀模式可能會增加程式碼的複雜度和冗餘度,因此需要謹慎使用。

 

21. 存取者模式


存取者模式(Visitor Pattern):是一種行為型設計模式,用於將操作與其所操作的物件分離開來。該模式的核心思想是將操作封裝在一個存取者物件中,而不是分散在各個物件中。通過將操作與物件分離開來,存取者模式可以在不修改物件結構的情況下,新增新的操作。

在前端開發中,存取者模式通常用於處理DOM樹上的操作。由於DOM樹結構通常很深,而且節點型別不同,因此對DOM樹進行一系列的操作,常常需要寫很多程式碼。而存取者模式可以將這些操作抽象出來,封裝到存取者物件中,從而簡化了程式碼量。

在存取者模式中,有兩種角色:存取者(Visitor)和被存取者(Element)。被存取者是一組物件,它們具有不同的介面,用於接受存取者的存取。存取者則是一組物件的操作,用於處理被存取者。存取者通常會遍歷整個被存取者的結構,並對每個節點進行操作。

下面是一個簡單的存取者模式範例:

// 定義被存取者
class Element {
  accept(visitor) {
    visitor.visit(this);
  }
}

// 定義存取者
class Visitor {
  visit(element) {
    // 處理元素
  }
}

// 定義具體的元素
class ConcreteElement extends Element {
  // 具體的實現
}

// 定義具體的存取者
class ConcreteVisitor extends Visitor {
  visit(element) {
    // 處理具體的元素
  }
}

// 使用存取者模式
const element = new ConcreteElement();
const visitor = new ConcreteVisitor();
element.accept(visitor);

在這個例子中,我們首先定義了一個被存取者 Element,它有一個 accept 方法,用於接受存取者的存取。然後定義了一個存取者 Visitor,它有一個 visit 方法,用於處理被存取者 Element。接下來,我們定義了具體的元素 ConcreteElement 和具體的存取者 ConcreteVisitor,並實現了它們的具體邏輯。最後,在主程式中,我們建立了一個 ConcreteElement 範例和一個 ConcreteVisitor 範例,將 ConcreteElement 範例作為引數傳遞給 ConcreteVisitorvisit 方法。

總之,存取者模式可以將操作和物件分離開來,從而實現程式碼的解耦和靈活性。在前端開發中,它常常被用來處理複雜的DOM樹結構。

 

22. 委託模式


委託模式(Delegation pattern):將一個物件的某個方法委託給另一個物件來執行,它可以幫助我們將物件之間的關係更加靈活地組織起來,從而提高程式碼的可維護性和複用性。

在委託模式中,一個物件(稱為委託物件)將一些特定的任務委託給另一個物件(稱為代理物件)來執行。代理物件通常具有和委託物件相同的介面,因此可以完全替代委託物件,而且可以根據需要動態地改變委託物件,從而實現了物件之間的鬆耦合。

在實際應用中,委託模式常常和其他模式一起使用,比如組合模式、單例模式、觀察者模式等。例如,我們可以使用委託模式來實現組合模式中的葉節點和枝節點的統一介面,從而實現對整個樹形結構的遞迴遍歷。

下面是一個使用委託模式的簡單範例:

// 委託物件
const delegate = {
  greet(name) {
    return `Hello, ${name}!`;
  }
};

// 代理物件
const proxy = {
  delegate: delegate,
  greet(name) {
    return this.delegate.greet(name);
  }
};

// 使用代理物件
console.log(proxy.greet("world")); // 輸出:Hello, world!

在上面的例子中,我們定義了一個委託物件 delegate,它有一個 greet 方法用於向指定的名稱打招呼。然後,我們又定義了一個代理物件 proxy,它將委託物件儲存在自己的屬性 delegate 中,並且實現了和委託物件相同的 greet 方法,但是它的實現其實是通過呼叫委託物件的 greet 方法來實現的。

最後,我們通過呼叫代理物件的 greet 方法來向世界打招呼,實際上代理物件內部會委託給委託物件來執行這個任務。

 

23. 計算屬性模式


計算屬性模式(Computed Property Pattern):在JavaScript中,可以使用Object.defineProperty()方法來實現計算屬性模式,通過get和set方法來計算屬性值。

計算屬性模式用於將物件的某些屬性值與其他屬性值相關聯。該模式常用於Vue.js等框架中。

計算屬性模式的基本思想是,定義一個函數作為物件的屬性,並在該函數中計算出相關聯的屬性值。當存取該屬性時,實際上是呼叫該函數並返回計算結果。

例如,假設有一個物件包含長度和寬度屬性,需要計算出它們的面積。可以定義一個計算屬性area,該屬性為一個函數,返回長度和寬度的乘積:

const rectangle = {
  length: 10,
  width: 5,
  get area() {
    return this.length * this.width;
  }
};

console.log(rectangle.area); // 50

在上面的程式碼中,當存取rectangle物件的area屬性時,實際上是呼叫了該物件的area函數,並返回計算結果50。

計算屬性模式的優點是,可以使物件屬性之間的關係更加清晰和易於維護。例如,在上面的例子中,如果需要修改面積計算公式,只需要修改area函數即可,而不需要修改存取該屬性的程式碼。

需要注意的是,在計算屬性模式中,計算屬性的值並不是固定的,而是根據其他屬性值的變化而變化的。因此,在物件的屬性值發生變化時,需要確保計算屬性的值也能及時更新。在Vue.js等框架中,可以通過watcher等機制來實現自動更新。

 

24. 路由模式


路由模式(Router Pattern):將頁面的不同狀態對映到不同的URL路徑上,使得使用者可以直接通過URL來存取頁面的不同狀態。

路由模式通常用於實現單頁面應用(SPA)的頁面導航和狀態管理。具體來說,路由模式通過解析URL路徑來確定應該顯示哪個頁面,並使用歷史記錄API來管理頁面狀態。

一般來說,路由模式包含以下幾個關鍵部分:

  1. 路由表:定義URL路徑與頁面元件的對映關係。
  2. 路由器:負責監聽URL路徑的變化,根據路由表匹配對應的頁面元件,並將其渲染到頁面上。
  3. 歷史記錄管理器:負責管理瀏覽器的歷史記錄,以便使用者可以使用瀏覽器的前進和後退按鈕導航應用程式的不同狀態。

常見的前端路由框架有Vue Router、React Router和Angular Router等。這些框架都提供了一系列API和元件來幫助開發者快速構建SPA應用程式,並實現靈活、可延伸的路由功能。

下面是一個基於Vue Router的簡單路由範例:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/about',
    component: About
  },
  {
    path: '/contact',
    component: Contact
  }
]

const router = new VueRouter({
  routes
})

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

首先,我們需要在Vue專案中安裝並匯入Vue Router模組,然後,我們可以定義一個路由表,指定每個URL路徑對應的頁面元件,在這個範例中,我們定義了三個路由路徑,分別對應HomeAboutContact三個頁面元件。

接著,我們可以建立一個VueRouter範例,並將路由表作為引數傳遞進去,最後,我們需要將VueRouter範例掛載到Vue根範例中。

現在,我們就可以在應用程式中使用路由了。例如,我們可以在App.vue元件中新增一個路由出口,並使用<router-link>元件來定義導航連結:

<template>
  <div id="app">
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-link to="/contact">Contact</router-link>
    <router-view></router-view>
  </div>
</template>

在這個範例中,<router-view>元件將渲染當前路由路徑對應的頁面元件。當用戶點選導航連結時,Vue Router將自動更新URL路徑,並根據路由表匹配對應的頁面元件進行渲染。

 

25. 直譯器模式


直譯器模式(Interpreter Pattern):是一種行為型設計模式,它可以用來解決一些特定問題,例如編譯器、計算器等等。這種模式定義了一個語言的語法,並用一個直譯器來解釋語言中的表示式。

直譯器模式可以用來處理例如資料格式化、表單驗證等業務場景。在這些場景中,我們需要定義一些語法規則,然後使用直譯器來解釋這些規則。

直譯器模式的基本結構包括四個角色:抽象表示式、終結符表示式、非終結符表示式和上下文。

  • 抽象表示式定義了一個抽象的介面,用於解釋表示式。
  • 終結符表示式是最基本的表示式,它代表了語言中的一個單一的符號,例如一個變數或者一個數位。
  • 非終結符表示式則是由多個終結符表示式組成的表示式,它代表了複雜的語言語法規則。
  • 上下文用於儲存直譯器解釋時的中間結果。

在使用直譯器模式時,我們需要先定義好語言的語法規則,然後再根據這些規則建立相應的表示式物件,並將其組合成一個完整的表示式樹。最後,我們可以使用直譯器來解釋這棵表示式樹,並得到相應的結果。

以下是一個簡單的範例,演示瞭如何使用直譯器模式來處理一個簡單的算術表示式。在這個範例中,我們定義了一個語法規則,用於表示加法和減法運算,並使用直譯器模式來解釋這個表示式。

// 定義抽象表示式
class Expression {
  interpret() {}
}

// 定義終結符表示式
class NumberExpression extends Expression {
  constructor(number) {
    super();
    this.number = number;
  }

  interpret() {
    return this.number;
  }
}

// 定義非終結符表示式
class AddExpression extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret() {
    return this.left.interpret() + this.right.interpret();
  }
}

class SubtractExpression extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret() {
    return this.left.interpret() - this.right.interpret();
  }
}

// 定義上下文
class Context {
  constructor() {
    this.expression = null;
  }

  setExpression(expression) {
    this.expression = expression;
  }

  evaluate() {
    return this.expression.interpret();
  }
}

// 使用範例
const context = new Context();
const expression = new SubtractExpression(
  new AddExpression(new NumberExpression(10), new NumberExpression(5)),
  new NumberExpression(2)
);
context.setExpression(expression);
console.log(context.evaluate()); // 輸出 13

在這個範例中,我們定義了四個表示式類:ExpressionNumberExpressionAddExpression 和 SubtractExpression,並分別實現了它們的 interpret() 方法。同時,我們還定義了一個上下文類 Context,用於儲存直譯器解釋時的中間結果。

在範例的最後,我們使用 SubtractExpressionAddExpression 和 NumberExpression 等表示式物件來建立一個表示式樹,並將其儲存在上下文中。最後,我們使用 Context 物件的 evaluate() 方法來求出表示式的值,並輸出結果。

 

26. 享元模式


享元模式(Flyweight Pattern):是一種用於優化物件建立和管理的設計模式。它旨在減少記憶體消耗和提高效能,通過共用具有相同狀態的物件來實現這一目標。

具體來說,享元模式涉及兩個主要的物件:享元工廠和具有共用狀態的享元物件。享元工廠負責建立和管理共用物件,以確保每個物件只被建立一次。享元物件則包含需要共用的狀態資訊,並提供介面以存取該狀態。

通過使用享元模式,可以顯著減少記憶體消耗和提高效能,尤其是在處理大量相似物件時。常見的使用享元模式的場景包括:DOM元素的複用、快取資料、減少ajax請求等。

需要注意的是,享元模式雖然可以優化記憶體和效能,但是也可能會犧牲一定的可讀性和維護性。因此,應該在合適的場景下使用該模式。

以下是一個使用享元模式的簡單範例,其中我們建立了一個享元工廠和一個具有共用狀態的享元物件:

// 定義享元工廠
const FlyweightFactory = function () {
  const flyweights = {};

  const get = function (key) {
    if (flyweights[key]) {
      return flyweights[key];
    }
    const flyweight = {
      // 共用的狀態資訊
      key: key,
      // 具體的操作方法
      operation: function () {
        console.log('Executing operation for key: ' + this.key);
      }
    };
    flyweights[key] = flyweight;
    return flyweight;
  };

  return {
    get: get
  };
};

// 使用享元工廠建立享元物件
const factory = new FlyweightFactory();
const flyweight1 = factory.get('key1');
const flyweight2 = factory.get('key2');

// 呼叫共用的操作方法
flyweight1.operation(); // 輸出: "Executing operation for key: key1"
flyweight2.operation(); // 輸出: "Executing operation for key: key2"

在上面的範例中,我們定義了一個名為 FlyweightFactory 的享元工廠,並實現了 get 方法來獲取共用狀態的享元物件。當請求一個新的享元物件時,我們首先檢查它是否已經存在於工廠的內部快取中,如果存在則返回它,否則建立一個新的物件並將其新增到快取中。

我們然後使用 factory 範例來建立兩個享元物件 flyweight1 和 flyweight2,它們分別具有鍵值為 'key1' 和 'key2' 的共用狀態資訊。最後,我們呼叫每個物件的 operation 方法來執行共用的操作。

值得注意的是,在上面的範例中,我們建立了兩個不同的享元物件,因為它們具有不同的鍵值。如果我們嘗試再次獲取具有相同鍵值的物件,將會返回已存在的物件,而不是建立一個新的。這就是享元模式的核心思想——通過共用具有相同狀態的物件來減少記憶體消耗和提高效能。

 

27. 依賴注入模式


依賴注入模式(Dependency Injection Pattern):允許我們通過將物件的依賴關係從程式碼中分離出來,從而使程式碼更加模組化和可重用。

在傳統的程式設計模式中,一個物件可能會直接建立或者獲取它需要的其他物件,這樣會造成物件之間的緊耦合關係,難以維護和擴充套件。而使用依賴注入模式,則可以將物件的依賴關係從物件內部移到外部,從而實現鬆耦合的設計,便於維護和擴充套件。

依賴注入模式可以通過建構函式、屬性、方法等方式來實現。在前端開發中,通常使用框架(如Angular、Vue、React等)來實現依賴注入,這些框架提供了依賴注入容器,可以自動管理物件之間的依賴關係。

以下是一個使用依賴注入模式的範例程式碼:

class UserService {
  constructor(apiService) {
    this.apiService = apiService;
  }
  
  getUser(id) {
    return this.apiService.get(`/users/${id}`);
  }
}

class ApiService {
  constructor(httpService) {
    this.httpService = httpService;
  }
  
  get(url) {
    return this.httpService.get(url);
  }
}

class HttpService {
  get(url) {
    // 傳送HTTP請求
  }
}

const httpService = new HttpService();
const apiService = new ApiService(httpService);
const userService = new UserService(apiService);

userService.getUser(1);

在上面的程式碼中,UserServiceApiServiceHttpService三個類之間都存在依賴關係。使用依賴注入模式,可以將這些依賴關係從內部移到外部,從而實現物件之間的解耦。在範例化UserService物件時,將依賴的ApiService物件作為引數傳入建構函式;在範例化ApiService物件時,將依賴的HttpService物件作為引數傳入建構函式。這樣就實現了依賴注入。


每個設計模式都有其適用的場景和優缺點,需要根據具體情況來選擇使用。其實,還有很多其他設計模式,MVC模式(Model-View-Controller)MVVM模式(Model-View-ViewModel)元件模式(Component Pattern)等等,這裡就不多介紹了,上文講到的面試肯定夠用了,但要真正融合進自己的專案中,還要多思考多理解多實踐,認識和應用設計模式可以幫助我們編寫更好的程式碼,提高程式碼的可讀性、可維護性和可延伸性。