如何彻底掌握 JavaScript 23种设计模式

  • 设计模式是解决特定问题的常用解决方案,它们可以帮助开发者编写更清晰、可维护、可扩展的代码。在 JavaScript 中,常见的设计模式可以分为三大类:创建型模式、结构型模式 和 行为型模式。本文将全面介绍 JavaScript 中常见的设计模式,帮助你更好地理解它们的应用场景

构造器模式

  • 在 ES6 引入 class 语法后,构造器模式变得更简洁。class 是 JavaScript 中的语法糖,底层依然是通过构造函数和原型链实现的。
class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(
      `Hello, my name is ${this.name} and I am ${this.age} years old.`,
    );
  }
}

const person = new Person('tom', 18);
person.greet();
  • class:ES6 引入的 class 语法提供了更清晰的构造函数定义方式。
  • constructor 方法:constructor 方法相当于传统的构造函数,初始化类的属性。
  • 方法定义:在 class 中,所有的方法会自动添加到 prototype 上,供实例共享。

原型模式

  • 原型模式(Prototype Pattern)是一种创建对象的设计模式。它通过复制现有对象的实例来创建新的对象,而不是通过构造函数来实例化对象。这种模式非常适合在不知道要创建多少对象的情况下使用,或者对象的创建成本较高时。
  • 在 JavaScript 中,所有对象都可以作为原型,通过原型链来实现继承和属性共享。使用原型模式可以提高对象创建的效率,尤其是在需要创建大量相似对象的情况下。
const carPrototype = {
  init(brand, model) {
    this.brand = brand;
    this.model = model;
  },
  getDetails() {
    return `Car brand: ${this.brand}, model: ${this.model}`;
  },
  clone() {
    const newCar = Object.create(this);
    newCar.init(this.brand, this.model);
    return newCar;
  },
};

// 创建一个原型对象
const car1 = Object.create(carPrototype);
car1.init('Toyota', 'Corolla');

console.log(car1.getDetails()); // 输出: Car brand: Toyota, model: Corolla

// 使用原型模式克隆一个新对象
const car2 = car1.clone();
console.log(car2.getDetails()); // 输
  1. 原型模式的优势
  • 高效性:原型模式可以避免重复的对象创建过程,通过克隆现有对象提高效率。
  • 内存节省:多个对象共享相同的方法,节省内存空间。
  • 灵活性:可以轻松扩展原型对象,添加新的方法和属性,所有克隆的对象都能访问这些更新。
  1. 原型模式的应用场景
  • 对象创建成本较高:当创建对象的开销较大时,使用原型模式可以通过克隆已有对象来减少开销。
  • 需要频繁创建相似对象:在需要创建许多相似对象时,使用原型模式可以提高效率。
  • 实现原型链:JavaScript 的继承机制是基于原型的,原型模式在 JavaScript 中具有天然的优势。
  1. 原型模式与构造器模式的对比
特点 原型模式 构造器模式
对象创建方式 通过克隆已有对象 通过构造函数创建新对象
方法共享 通过原型对象共享方法 通过 prototype 共享方法
内存占用 节省内存,通过共享方法 每个实例都有独立的方法拷贝
使用场景 对象创建成本高,需频繁创建相似对象 创建简单对象,需初始化属性

工厂模式

  • 工厂模式(Factory Pattern)是一种用于创建对象的设计模式,它允许通过接口或函数来创建对象,而不需要显式地指定对象的类或构造函数。工厂模式的核心思想是将对象的创建逻辑集中到一个工厂函数中,调用者无需关心对象的具体创建过程。
function CarFactory(type) {
  let car;

  switch (type) {
    case 'sedan':
      car = { type: 'Sedan', wheels: 4, doors: 4 };
      break;
    case 'suv':
      car = { type: 'SUV', wheels: 4, doors: 5 };
      break;
    case 'truck':
      car = { type: 'Truck', wheels: 6, doors: 2 };
      break;
    default:
      car = { type: 'Unknown', wheels: 4, doors: 4 };
  }

  car.drive = function () {
    console.log(`Driving a ${this.type}`);
  };

  return car;
}

// 使用工厂创建不同类型的汽车
const sedan = CarFactory('sedan');
const suv = CarFactory('suv');
const truck = CarFactory('truck');

sedan.drive(); // 输出: Driving a Sedan
suv.drive(); // 输出: Driving a SUV
truck.drive(); // 输出: Driving a Truck
  • 关键点:
  • 工厂函数:CarFactory 是工厂函数,它根据输入的参数创建不同类型的对象。
  • 封装创建逻辑:创建对象的过程被封装在工厂函数中,调用者只需要传递参数,而不需要关心对象的创建细节。
  • 灵活性:可以根据输入的不同,生成不同类型的对象。
  1. 工厂模式的优势
  • 封装复杂的创建逻辑:工厂模式将对象的创建逻辑集中封装,避免在代码中多次重复相同的创建过程。
  • 解耦对象创建与使用:调用者无需知道对象的构造细节,只需关心工厂提供的接口即可。
  • 易于扩展:可以很容易地扩展工厂函数,加入新的对象类型,而不会影响现有代码。
  1. 工厂模式的应用场景
  • 创建复杂对象:当对象的创建过程复杂,需要初始化很多属性时,工厂模式可以简化对象创建。
  • 根据条件创建不同对象:当需要根据不同条件创建不同对象时,工厂模式是一种很好的解决方案。
  • 隐藏对象构造的复杂性:工厂模式可以隐藏对象的构造细节,使代码更易于维护和修改。

抽象工厂模式

  • 抽象工厂模式是工厂模式的扩展,它允许创建一组相关或依赖的对象,而无需指定它们的具体类。抽象工厂模式提供了一种抽象层,使得工厂可以创建不同类型的对象,具体对象的创建细节交由子类或具体工厂实现。
function CarFactory() {}

CarFactory.prototype.createCar = function () {
  throw new Error('This method should be overridden!');
};

//  Sedan 工厂
function SedanFactory() {}
SedanFactory.prototype = Object.create(CarFactory.prototype);
SedanFactory.prototype.createCar = function () {
  return { type: 'Sedan', wheels: 4, doors: 4 };
};

//  SUV 工厂
function SUVFactory() {}
SUVFactory.prototype = Object.create(CarFactory.prototype);
SUVFactory.prototype.createCar = function () {
  return { type: 'SUV', wheels: 4, doors: 5 };
};

// 使用抽象工厂创建汽车
const sedanFactory = new SedanFactory();
const suvFactory = new SUVFactory();

const sedan = sedanFactory.createCar();
const suv = suvFactory.createCar();

console.log(sedan); //  输出: { type: 'Sedan', wheels: 4, doors: 4 }
console.log(suv); //  输出: { type: 'SUV', wheels: 4, doors:
  1. 工厂模式与构造器模式的区别
  • 工厂模式:通过工厂函数创建对象,调用者不需要直接使用 new 关键字,创建逻辑被封装在工厂内部。
  • 构造器模式:通过构造函数直接创建对象,使用 new 关键字实例化。
特点 工厂模式 构造器模式
对象创建方式 通过工厂函数创建 通过构造函数创建
使用 new 关键字 不需要使用 需要使用
封装创建逻辑 封装复杂的对象创建逻辑 创建过程公开
灵活性 根据条件动态创建不同类型对象 一般用于创建单一类型对象

建造者模式

  • 建造者模式(Builder Pattern)是一种创建型设计模式,用于构造复杂对象,将对象的构造过程与其表示分离。通过建造者模式,客户端可以一步步构建一个复杂的对象,而不必关心内部的具体细节和实现过程。
  • 建造者模式非常适合在需要构建对象时涉及多个步骤的场景,或者当对象有很多可选属性时。与工厂模式相比,建造者模式更注重对象的构造过程,而不是简单的对象创建。
// 产品类,表示要创建的复杂对象
class House {
  constructor() {
    this.floors = 0;
    this.windows = 0;
    this.garden = false;
  }
}

// 建造者类,负责构造复杂对象
class HouseBuilder {
  constructor() {
    this.house = new House();
  }

  buildFloors(floors) {
    this.house.floors = floors;
    return this; // 返回 this 以支持链式调用
  }

  buildWindows(windows) {
    this.house.windows = windows;
    return this; // 返回 this 以支持链式调用
  }

  buildGarden(hasGarden) {
    this.house.garden = hasGarden;
    return this;
  }

  // 返回构建好的对象
  build() {
    return this.house;
  }
}

// 使用建造者模式构建对象
const house = new HouseBuilder()
  .buildFloors(2)
  .buildWindows(4)
  .buildGarden(true)
  .build();

console.log(house);
// 输出: { floors: 2, windows: 4, garden: true }
  • 关键点:
  • 产品类:House 是最终构建的复杂对象,表示房子。
  • 建造者类:HouseBuilder 包含了逐步构建 House 对象的逻辑,提供了多个方法来一步步设置对象的属性。
  • 链式调用:每个构造方法返回 this,使得建造步骤可以链式调用,构建过程更简洁。
  • build() 方法:最终通过 build() 方法返回构建好的对象。
  1. 建造者模式的优势
  • 分步创建复杂对象:将对象的构造步骤分离出来,使得构建复杂对象变得简单和可控。
  • 可读性强:通过链式调用,建造过程的每一步都清晰明了,增强代码的可读性。
  • 解耦构造与表示:建造者模式将对象的构造过程与对象的表示分离,使构建过程可以独立扩展。
  1. 建造者模式的应用场景
  • 构建复杂对象:对象的创建过程需要多个步骤时,可以通过建造者模式简化过程。
  • 对象有很多可选参数:在构建有许多可选属性的对象时,使用建造者模式可以避免传递过多的构造函数参数。
  • 对象的构造逻辑较复杂:当构造对象涉及到许多中间步骤时,建造者模式可以将这些步骤分离,使构造逻辑更加清晰。
  1. 建造者模式与工厂模式的对比
特点 建造者模式 工厂模式
对象创建方式 按步骤逐步构建对象 通过工厂函数直接创建对象
使用场景 构建复杂对象,多个步骤,多个可选属性 创建简单对象,根据条件返回不同对象
方法调用顺序 可以控制调用顺序,每个步骤可选 调用工厂函数一次,返回对象
链式调用 支持链式调用,按步骤设置属性 一般不涉及链式调用
  1. 建造者模式的实际应用
  • 建造者模式在需要构建复杂对象、需要可选参数的场景中非常实用。以下是一些常见的应用场景:
  • 构建包含多个步骤的复杂对象:例如创建一份详细的文档、生成复杂的 UI 组件或对象需要多个可选参数时。
  • 构造复杂的配置对象:在配置系统中,通常需要很多可选参数来定制配置,建造者模式可以很好地应对这种需求。
  • 简化构造函数的调用:当构造函数参数过多时,建造者模式可以避免构造函数变得过于复杂,通过逐步设置属性简化调用。

单例模式

  • 单例模式(Singleton Pattern)是一种常见的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这在一些需要全局共享资源的场景下非常有用,比如全局配置、日志记录器、数据库连接等。
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
  }
}

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

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

关键点:
闭包:通过闭包创建一个私有的 instance 变量,外部无法直接访问。
惰性实例化:getInstance 方法只有在需要时才创建实例,并且每次返回的都是同一个实例

  1. 例模式的优势和劣势
  • 优势:
  1. 节省资源:由于单例模式只允许创建一个实例,因此在需要共享资源时非常高效,比如数据库连接池。
  2. 全局访问点:提供一个全局唯一的访问点,确保全局状态的统一。
  3. 防止重复实例化:避免多次实例化带来的问题,保证系统中只有一个实例存在。
  • 劣势:
  1. 难以扩展:单例模式由于只允许创建一个实例,可能会限制其扩展性。

  2. 全局状态:在某些情况下,全局的共享实例可能会导致状态管理的复杂性。

  3. 难以测试:由于单例模式持有状态,在测试时难以隔离环境,可能导致测试依赖全局状态。

  4. 适用场景

单例模式适用于以下场景:

  • 日志系统:日志系统需要在整个应用程序中保持一个唯一的日志对象,方便记录日志。
  • 全局配置对象:当应用程序需要共享一些全局配置时,可以通过单例模式实现统一的配置管理。
  • 数据库连接池:在服务端应用中,使用单例模式可以确保只创建一个数据库连接池实例,节省资源。
  • 浏览器中的本地存储管理:在前端开发中,可能需要一个全局对象来管理 localStorage 或 sessionStorage。

装饰器模式

  • 装饰器模式(Decorator Pattern)是一种结构型设计模式,用于在不改变对象本身的情况下,动态地给对象添加新功能。这种模式可以让我们灵活地为对象增添职责,并且避免了创建子类来扩展功能的繁琐。
  • 装饰器模式的核心思想:
  1. 动态扩展功能:装饰器为对象提供了额外的行为,而不改变对象的原始结构。
  2. 组合而非继承:通过组合装饰器对象,可以灵活地扩展功能,而不是通过继承来增加复杂度。
class Car {
  drive() {
    console.log('The car is driving.');
  }
}

// 装饰器类
class CarDecorator {
  constructor(car) {
    this.car = car;
  }

  drive() {
    console.log('Starting the engine...');
    this.car.drive();
    console.log('Turning on the headlights.');
  }
}

const myCar = new Car();
const decoratedCar = new CarDecorator(myCar);

decoratedCar.drive();
  1. 装饰器模式的优势和劣势
  • 优势:
  1. 灵活性高:装饰器模式允许我们动态添加功能,而不需要修改对象的代码。这让功能扩展变得更加灵活。
  2. 遵循开闭原则:对象可以通过添加装饰器来扩展功能,而不需要修改其原有代码。
  3. 可以组合:多个装饰器可以叠加使用,形成功能的组合,这比继承链更加灵活和易于维护。
  • 劣势:
  1. 复杂性增加:装饰器模式可能会导致系统中增加大量的装饰器类或函数,增加代码的复杂性。

  2. 调试困难:由于装饰器是动态添加行为,调试时很难直接看到对象的真实状态。

  3. 装饰器模式的应用场景

装饰器模式非常适合以下场景:

  1. 日志记录:为函数或方法添加日志记录,而不修改原函数代码。
  2. 数据验证:在函数执行前动态添加数据验证逻辑。
  3. 权限控制:为某些方法添加权限检查功能,例如确保用户具有某些权限才能调用特定功能。
  4. 函数节流:限制函数的调用频率,可以使用装饰器在函数外部添加节流逻辑。

适配器模式

  • 适配器模式(Adapter Pattern)是一种结构型设计模式,主要用于解决接口不兼容的问题。适配器通过包装一个对象,使其与客户端期望的接口兼容,从而允许原本不兼容的对象协同工作。
  • 适配器模式的核心思想:
  1. 将一个类的接口转换为另一个客户端希望的接口。
  2. 使得原本由于接口不兼容而无法一起工作的类可以协同工作。
// 第三方库提供的类
class ThirdPartyApi {
  send() {
    return 'Sending data via ThirdPartyApi';
  }
}

// 我们系统需要的接口
class MySystemApi {
  request() {
    return 'Sending data via MySystemApi';
  }
}

// 适配器类,适配第三方 API 到我们的系统
class ApiAdapter {
  constructor(thirdPartyApi) {
    this.thirdPartyApi = thirdPartyApi;
  }

  request() {
    // 调用第三方 API 的 send 方法,但暴露给客户端的是 request 方法
    return this.thirdPartyApi.send();
  }
}

const thirdPartyApi = new ThirdPartyApi();
const adaptedApi = new ApiAdapter(thirdPartyApi);

console.log(adaptedApi.request()); // 输出:Sending data via ThirdPartyApi
  1. 适配器模式的优势和劣势
  • 优势:
  1. 提高兼容性:适配器模式可以帮助不同接口之间的协作,使得旧代码与新系统无缝对接。
  2. 遵循开闭原则:适配器模式允许我们在不修改原有类的情况下,为其增加新的接口兼容性。
  3. 解耦:通过适配器,客户端与具体实现解耦,可以更灵活地使用不同接口。
  • 劣势:
  1. 增加代码复杂度:为每个不兼容的接口创建适配器可能会导致代码量增加,尤其在大型系统中。

  2. 性能开销:适配器模式需要引入一层中间处理逻辑,可能会带来一定的性能开销。

  3. 适配器模式的应用场景
  • 适配器模式广泛应用于以下场景:
  1. 老系统与新系统的集成:在大型企业系统中,老旧系统与新系统的接口通常不兼容,适配器模式可以帮助它们无缝协作。
  2. 第三方库的封装:使用第三方库时,库的接口可能不符合项目的标准,可以通过适配器来包装这些接口,提供符合项目标准的接口。
  3. 兼容不同接口的实现:当需要同时支持多个不兼容的接口时,可以使用适配器进行转换。

策略模式

  • 策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并且使它们可以互相替换。策略模式让算法独立于使用它的客户端独立变化。这样我们可以在运行时选择合适的算法,而不需要修改客户端代码。

策略模式的核心思想:

  1. 将算法封装成独立的策略,并通过接口进行调用。
  2. 允许算法之间可以互换使用,而不会影响使用它们的客户端。
// 定义不同的策略
class RegularStrategy {
  calculate(price) {
    return price; // 正常价格,无折扣
  }
}

class SaleStrategy {
  calculate(price) {
    return price * 0.9; // 打9折
  }
}

class PremiumStrategy {
  calculate(price) {
    return price * 0.8; // 打8折
  }
}

// Context,负责根据不同策略进行计算
class PriceContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  calculatePrice(price) {
    return this.strategy.calculate(price);
  }
}

// 使用策略模式
const price = 100;
const priceContext = new PriceContext(new RegularStrategy());

console.log(priceContext.calculatePrice(price)); // 输出:100

// 切换到打折策略
priceContext.setStrategy(new SaleStrategy());
console.log(priceContext.calculatePrice(price)); // 输出:90

// 切换到高级会员折扣策略
priceContext.setStrategy(new PremiumStrategy());
console.log(priceContext.calculatePrice(price)); // 输出:80

关键点:

  • 策略类:RegularStrategy、SaleStrategy 和 PremiumStrategy 是不同的策略类,它们实现了相同的接口(calculate 方法)。
  • 上下文类:PriceContext 是上下文类,负责根据不同的策略类计算价格。
  • 动态选择策略:我们可以在运行时动态选择策略,而不需要修改 PriceContext 的内部逻辑。
  1. 策略模式的优势和劣势

优势:

  • 开闭原则:通过将算法封装到独立的策略类中,可以在不修改客户端代码的情况下添加新的策略。
  • 避免条件语句:使用策略模式可以避免在代码中编写大量的条件语句,增强代码的可维护性和可读性。
  • 更灵活的算法选择:客户端可以根据不同的条件动态选择不同的策略。
    劣势:
  • 增加类的数量:每一个策略都是一个独立的类,这可能会导致类的数量增加,从而增加系统的复杂性。
  • 策略类之间的差异难以控制:策略类的算法可能差异较大,难以统一处理,尤其在涉及多个复杂策略时。
  1. 策略模式的应用场景

策略模式适用于以下场景:

  1. 算法变体很多:当一个算法有多个实现方式,或者算法会频繁更改时,可以使用策略模式来灵活选择算法。
  2. 避免条件分支过多:当一个类中包含大量的条件分支(如 if…else 或 switch),可以考虑使用策略模式代替这些条件分支。
  3. 需要动态选择算法:当算法需要根据不同的条件在运行时进行切换时,可以使用策略模式。

代理模式

  • 代理模式(Proxy Pattern)是一种结构型设计模式,它为对象提供一个代理(即替身),并控制客户端对原始对象的访问。代理对象可以在客户端与目标对象之间进行一些额外的操作,如控制访问权限、延迟加载、缓存、日志记录等。

代理模式的核心思想:

  • 代理对象 作为客户端与目标对象之间的中介,它可以控制对目标对象的访问。
  • 通过代理对象,可以在访问目标对象前后进行一些额外操作。
// 目标对象
class RealSubject {
  request() {
    return 'Request from RealSubject';
  }
}

// 代理对象
class ProxySubject {
  constructor(realSubject) {
    this.realSubject = realSubject;
  }

  request() {
    // 执行一些额外的操作
    console.log('Proxy: Checking access before forwarding the request.');

    // 调用真实对象的请求
    const result = this.realSubject.request();

    // 执行一些后续操作
    console.log('Proxy: Logging the result after forwarding the request.');
    return result;
  }
}

// 客户端代码
const realSubject = new RealSubject();
const proxy = new ProxySubject(realSubject);

console.log<
上一篇:如何使用ssm实现基于MVC构架的网上食品店的设计与实现+vue


下一篇:Linux下网络转发功能