- 设计模式是解决特定问题的常用解决方案,它们可以帮助开发者编写更清晰、可维护、可扩展的代码。在 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()); // 输
- 高效性:原型模式可以避免重复的对象创建过程,通过克隆现有对象提高效率。
- 内存节省:多个对象共享相同的方法,节省内存空间。
- 灵活性:可以轻松扩展原型对象,添加新的方法和属性,所有克隆的对象都能访问这些更新。
- 对象创建成本较高:当创建对象的开销较大时,使用原型模式可以通过克隆已有对象来减少开销。
- 需要频繁创建相似对象:在需要创建许多相似对象时,使用原型模式可以提高效率。
- 实现原型链:JavaScript 的继承机制是基于原型的,原型模式在 JavaScript 中具有天然的优势。
特点 | 原型模式 | 构造器模式 |
---|---|---|
对象创建方式 | 通过克隆已有对象 | 通过构造函数创建新对象 |
方法共享 | 通过原型对象共享方法 | 通过 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 是工厂函数,它根据输入的参数创建不同类型的对象。
- 封装创建逻辑:创建对象的过程被封装在工厂函数中,调用者只需要传递参数,而不需要关心对象的创建细节。
- 灵活性:可以根据输入的不同,生成不同类型的对象。
- 封装复杂的创建逻辑:工厂模式将对象的创建逻辑集中封装,避免在代码中多次重复相同的创建过程。
- 解耦对象创建与使用:调用者无需知道对象的构造细节,只需关心工厂提供的接口即可。
- 易于扩展:可以很容易地扩展工厂函数,加入新的对象类型,而不会影响现有代码。
- 创建复杂对象:当对象的创建过程复杂,需要初始化很多属性时,工厂模式可以简化对象创建。
- 根据条件创建不同对象:当需要根据不同条件创建不同对象时,工厂模式是一种很好的解决方案。
- 隐藏对象构造的复杂性:工厂模式可以隐藏对象的构造细节,使代码更易于维护和修改。
抽象工厂模式
- 抽象工厂模式是工厂模式的扩展,它允许创建一组相关或依赖的对象,而无需指定它们的具体类。抽象工厂模式提供了一种抽象层,使得工厂可以创建不同类型的对象,具体对象的创建细节交由子类或具体工厂实现。
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:
- 工厂模式:通过工厂函数创建对象,调用者不需要直接使用 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() 方法返回构建好的对象。
- 分步创建复杂对象:将对象的构造步骤分离出来,使得构建复杂对象变得简单和可控。
- 可读性强:通过链式调用,建造过程的每一步都清晰明了,增强代码的可读性。
- 解耦构造与表示:建造者模式将对象的构造过程与对象的表示分离,使构建过程可以独立扩展。
- 构建复杂对象:对象的创建过程需要多个步骤时,可以通过建造者模式简化过程。
- 对象有很多可选参数:在构建有许多可选属性的对象时,使用建造者模式可以避免传递过多的构造函数参数。
- 对象的构造逻辑较复杂:当构造对象涉及到许多中间步骤时,建造者模式可以将这些步骤分离,使构造逻辑更加清晰。
特点 | 建造者模式 | 工厂模式 |
---|---|---|
对象创建方式 | 按步骤逐步构建对象 | 通过工厂函数直接创建对象 |
使用场景 | 构建复杂对象,多个步骤,多个可选属性 | 创建简单对象,根据条件返回不同对象 |
方法调用顺序 | 可以控制调用顺序,每个步骤可选 | 调用工厂函数一次,返回对象 |
链式调用 | 支持链式调用,按步骤设置属性 | 一般不涉及链式调用 |
- 建造者模式在需要构建复杂对象、需要可选参数的场景中非常实用。以下是一些常见的应用场景:
- 构建包含多个步骤的复杂对象:例如创建一份详细的文档、生成复杂的 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 方法只有在需要时才创建实例,并且每次返回的都是同一个实例
- 优势:
- 节省资源:由于单例模式只允许创建一个实例,因此在需要共享资源时非常高效,比如数据库连接池。
- 全局访问点:提供一个全局唯一的访问点,确保全局状态的统一。
- 防止重复实例化:避免多次实例化带来的问题,保证系统中只有一个实例存在。
- 劣势:
-
难以扩展:单例模式由于只允许创建一个实例,可能会限制其扩展性。
-
全局状态:在某些情况下,全局的共享实例可能会导致状态管理的复杂性。
-
难以测试:由于单例模式持有状态,在测试时难以隔离环境,可能导致测试依赖全局状态。
-
适用场景
单例模式适用于以下场景:
- 日志系统:日志系统需要在整个应用程序中保持一个唯一的日志对象,方便记录日志。
- 全局配置对象:当应用程序需要共享一些全局配置时,可以通过单例模式实现统一的配置管理。
- 数据库连接池:在服务端应用中,使用单例模式可以确保只创建一个数据库连接池实例,节省资源。
- 浏览器中的本地存储管理:在前端开发中,可能需要一个全局对象来管理 localStorage 或 sessionStorage。
装饰器模式
- 装饰器模式(Decorator Pattern)是一种结构型设计模式,用于在不改变对象本身的情况下,动态地给对象添加新功能。这种模式可以让我们灵活地为对象增添职责,并且避免了创建子类来扩展功能的繁琐。
- 装饰器模式的核心思想:
- 动态扩展功能:装饰器为对象提供了额外的行为,而不改变对象的原始结构。
- 组合而非继承:通过组合装饰器对象,可以灵活地扩展功能,而不是通过继承来增加复杂度。
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();
- 优势:
- 灵活性高:装饰器模式允许我们动态添加功能,而不需要修改对象的代码。这让功能扩展变得更加灵活。
- 遵循开闭原则:对象可以通过添加装饰器来扩展功能,而不需要修改其原有代码。
- 可以组合:多个装饰器可以叠加使用,形成功能的组合,这比继承链更加灵活和易于维护。
- 劣势:
装饰器模式非常适合以下场景:
- 日志记录:为函数或方法添加日志记录,而不修改原函数代码。
- 数据验证:在函数执行前动态添加数据验证逻辑。
- 权限控制:为某些方法添加权限检查功能,例如确保用户具有某些权限才能调用特定功能。
- 函数节流:限制函数的调用频率,可以使用装饰器在函数外部添加节流逻辑。
适配器模式
- 适配器模式(Adapter Pattern)是一种结构型设计模式,主要用于解决接口不兼容的问题。适配器通过包装一个对象,使其与客户端期望的接口兼容,从而允许原本不兼容的对象协同工作。
- 适配器模式的核心思想:
- 将一个类的接口转换为另一个客户端希望的接口。
- 使得原本由于接口不兼容而无法一起工作的类可以协同工作。
// 第三方库提供的类
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
- 优势:
- 提高兼容性:适配器模式可以帮助不同接口之间的协作,使得旧代码与新系统无缝对接。
- 遵循开闭原则:适配器模式允许我们在不修改原有类的情况下,为其增加新的接口兼容性。
- 解耦:通过适配器,客户端与具体实现解耦,可以更灵活地使用不同接口。
- 劣势:
- 适配器模式广泛应用于以下场景:
- 老系统与新系统的集成:在大型企业系统中,老旧系统与新系统的接口通常不兼容,适配器模式可以帮助它们无缝协作。
- 第三方库的封装:使用第三方库时,库的接口可能不符合项目的标准,可以通过适配器来包装这些接口,提供符合项目标准的接口。
- 兼容不同接口的实现:当需要同时支持多个不兼容的接口时,可以使用适配器进行转换。
策略模式
- 策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并且使它们可以互相替换。策略模式让算法独立于使用它的客户端独立变化。这样我们可以在运行时选择合适的算法,而不需要修改客户端代码。
策略模式的核心思想:
- 将算法封装成独立的策略,并通过接口进行调用。
- 允许算法之间可以互换使用,而不会影响使用它们的客户端。
// 定义不同的策略
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 的内部逻辑。
优势:
- 开闭原则:通过将算法封装到独立的策略类中,可以在不修改客户端代码的情况下添加新的策略。
- 避免条件语句:使用策略模式可以避免在代码中编写大量的条件语句,增强代码的可维护性和可读性。
- 更灵活的算法选择:客户端可以根据不同的条件动态选择不同的策略。
劣势: - 增加类的数量:每一个策略都是一个独立的类,这可能会导致类的数量增加,从而增加系统的复杂性。
- 策略类之间的差异难以控制:策略类的算法可能差异较大,难以统一处理,尤其在涉及多个复杂策略时。
策略模式适用于以下场景:
- 算法变体很多:当一个算法有多个实现方式,或者算法会频繁更改时,可以使用策略模式来灵活选择算法。
- 避免条件分支过多:当一个类中包含大量的条件分支(如 if…else 或 switch),可以考虑使用策略模式代替这些条件分支。
- 需要动态选择算法:当算法需要根据不同的条件在运行时进行切换时,可以使用策略模式。
代理模式
- 代理模式(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(proxy.request());
// 输出:
// Proxy: Checking access before forwarding the request.
// Proxy: Logging the result after forwarding the request.
// Request from RealSubject
关键点:
- 目标对象:RealSubject 提供了核心功能(如 request 方法)。
- 代理对象:ProxySubject 控制对 RealSubject 的访问,在调用前后添加额外的逻辑。
通过代理对象,客户端可以透明地调用目标对象,同时代理对象可以拦截请求,并在合适的时机执行额外逻辑。
- 控制访问:代理模式可以控制对目标对象的访问,执行权限控制、延迟加载、缓存等功能。
- 开闭原则:通过代理模式,可以在不修改目标对象的情况下扩展其功能。
- 性能优化:虚拟代理和缓存代理可以有效减少系统资源的消耗和重复操作,提升性能。
劣势: - 增加复杂性:引入代理对象会增加系统的复杂性,尤其当代理逻辑复杂时,可能会导致代码难以维护。
- 延迟真实对象的操作:虚拟代理会延迟真实对象的初始化,有时可能会导致意外的行为。
- 延迟初始化:如按需加载资源,避免过早初始化开销较大的对象。
- 权限控制:在系统中,控制用户对某些敏感数据或操作的访问权限。
- 性能优化:通过缓存代理减少重复计算或请求的次数,提高系统的性能。
- 远程代理:在分布式系统中,代理对象可以代表远程服务,从而简化客户端与远程服务的交互。
观察者模式
- 观察者模式(Observer Pattern)是一种行为型设计模式,它定义了对象之间的一对多依赖关系,使得当一个对象的状态发生变化时,所有依赖它的对象都会自动得到通知并更新。这种模式常用于事件系统、数据绑定和实时通信等场景。
观察者模式的核心思想:
- 主体对象(Subject)维护一组依赖它的观察者对象(Observer)。
- 当主体对象发生变化时,它会通知所有观察者,从而更新观察者的状态。
// 主体对象(被观察者)
class Subject {
constructor() {
this.observers = [];
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter((obs) => obs !== observer);
}
// 通知所有观察者
notifyObservers(data) {
this.observers.forEach((observer) => observer.update(data));
}
}
// 观察者对象
class Observer {
constructor(name) {
this.name = name;
}
// 更新方法,当主体状态改变时调用
update(data) {
console.log(`${this.name} received update: ${data}`);
}
}
// 创建主体
const subject = new Subject();
// 创建观察者
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
// 注册观察者
subject.addObserver(observer1);
subject.addObserver(observer2);
// 通知观察者
subject.notifyObservers('New data available');
// 输出:
// Observer 1 received update: New data available
// Observer 2 received update: New data available
关键点:
- 主体对象:Subject 维护一个观察者列表,当状态发生变化时,它会调用每个观察者的 update 方法。
- 观察者对象:Observer 是依赖于主体对象的,它通过 update 方法响应主体的变化。
- 通过这种设计,当 Subject 的状态改变时,所有依赖它的 Observer 都会被通知并做出相应的处理。
优势:
- 解耦观察者和主体:观察者模式使得观察者与主体之间的关系松散耦合,主体不需要知道观察者的具体实现,只需通过通用的接口通知它们。
- 动态扩展:可以在运行时添加或移除观察者,灵活性高,便于系统扩展。
- 实时响应:观察者模式允许系统对变化进行实时响应,特别适用于需要动态更新的场景。
劣势: - 性能问题:当观察者数量较多时,通知的频率和开销可能较大,尤其在频繁变化的系统中,可能影响性能。
- 调试困难:在复杂的观察者模式中,由于观察者和主体的解耦,追踪问题和调试可能变得更加困难。
- 通知顺序不确定:观察者接收到通知的顺序可能不确定,如果对顺序有要求,可能需要额外处理。
观察者模式适用于以下场景:
- 事件驱动系统:比如浏览器中的事件模型,当用户触发一个事件时,多个监听器(观察者)会做出响应。
- 数据绑定和同步:在现代前端框架(如 Vue、React)中,观察者模式常用于双向数据绑定和组件状态管理,当数据变化时,视图会自动更新。
- 实时系统:在需要实时更新的应用(如股票价格更新、消息推送等)中,观察者模式可以及时通知客户端变化。
发布订阅模式
- 发布订阅模式(Publish-Subscribe Pattern)是一种消息传递机制,它允许多个对象之间通过事件调度进行通信。发布者将消息发送给中间的事件通道,订阅者则从事件通道中接收消息。通过这种模式,发布者和订阅者之间实现了解耦。
核心思想: - 发布者:负责发布事件或消息。
- 订阅者:负责接收和处理消息。
- 事件通道(消息中介):连接发布者和订阅者,负责转发消息。
- 与观察者模式不同,发布订阅模式是通过一个中介对象来传递消息,发布者和订阅者之间没有直接联系。
// 事件管理器(发布订阅系统)
class EventEmitter {
constructor() {
this.events = {};
}
// 订阅事件
subscribe(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
// 取消订阅
unsubscribe(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(l => l !== listener);
}
// 发布事件
publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener => listener(data));
}
}
// 创建发布订阅系统
const eventEmitter = new EventEmitter();
// 订阅者
function listener1(data) {
console.log('Listener 1 received:', data);
}
function listener2(data) {
console.log('Listener 2 received:', data);
}
// 订阅事件
eventEmitter.subscribe('message', listener1);
eventEmitter.subscribe('message', listener2);
// 发布事件
eventEmitter.publish('message', 'Hello, Subscribers!');
// 输出:
// Listener 1 received: Hello, Subscribers!
// Listener 2 received: Hello, Subscribers!
// 取消订阅
eventEmitter.unsubscribe('message', listener1);
// 再次发布事件
eventEmitter.publish('message', 'Another message');
// 输出:
// Listener 2 received: Another message
关键点:
- 发布者通过 publish 方法发布消息。
- 订阅者通过 subscribe 方法订阅特定事件,并在事件触发时执行回调函数。
- 取消订阅可以通过 unsubscribe 方法移除订阅者。
优势:
-
解耦:发布者和订阅者完全分离,互相不知道对方的存在,降低了代码的耦合性。
-
灵活性高:可以动态地添加或移除订阅者,发布事件时可以有多个订阅者响应。
-
扩展性好:适用于分布式系统、异步处理和复杂应用中的模块解耦。
劣势: -
性能开销:如果有大量订阅者,或事件发布频率很高,可能会造成性能问题。
-
难以调试:由于发布者和订阅者之间没有直接联系,调试和排查问题时可能较为复杂。
-
消息丢失:如果订阅者在事件发布之前没有订阅事件,可能会错过消息。
-
发布订阅模式的应用场景
发布订阅模式在以下场景中非常常见:
- 事件驱动编程:如浏览器中的事件模型、Node.js 的 EventEmitter。
- 消息队列:在微服务架构中,用于服务之间的解耦和通信。
- 实时通信:聊天室、实时数据推送等应用。
- 模块解耦:在复杂应用中,使用发布订阅模式可以减少模块之间的依赖。
模块模式
- 模块模式(Module Pattern)是一种创建私有作用域和封装代码的设计模式,用来组织和隔离代码,避免全局变量污染。它通过使用闭包,将模块内部的变量和方法隐藏起来,只暴露必要的接口给外部。
核心思想:
- 私有性:模块内部的变量和方法默认是私有的,外部无法直接访问。
- 封装:通过返回一个对象,将需要暴露的属性和方法以接口形式提供给外部。
- 闭包:利用闭包的特性,创建模块的私有作用域。
const module = (function () {
// 私有变量和方法
const privateVar = "I am private";
const privateMethod = function () {
console.log(privateVar);
};
return {
// 公共方法
publicMethod: function () {
privateMethod();
},
//公共属性
publicVar: "I am public",
};
})();
console.log(module.publicVar); // 输出: I am public
console.log(module.publicMethod()); // 输出: I am private
关键点:
- privateVar 和 privateMethod 是模块的私有成员,外部无法直接访问。
- publicMethod 和 publicVar 是模块的公共接口,外部可以访问和使用。
优势:
-
私有性和封装性:模块模式通过闭包隐藏内部实现细节,增强代码的安全性和可维护性。
-
减少全局污染:将代码封装在模块内,避免了不必要的全局变量污染。
-
易于维护:代码逻辑清晰,模块化的设计可以使得代码更容易维护和扩展。
劣势: -
私有成员无法访问:私有变量和方法无法被外部直接访问,如果需要测试或调试私有成员,可能需要额外的处理。
-
依赖于闭包:模块模式依赖闭包的特性,可能会增加内存占用,特别是在频繁创建和销毁模块时。
- 命名空间:使用模块模式可以创建命名空间,避免全局变量冲突和命名污染。
- 代码组织:大型项目可以通过模块模式将不同功能模块化,提升代码的可维护性。
- 依赖管理:模块模式可以通过依赖注入的方式管理依赖关系,增强代码的灵活性和扩展性。
桥接模式
- 桥接模式(Bridge Pattern)是一种结构型设计模式,目的是将抽象部分与实现部分分离,使它们可以独立变化。通过这种分离,抽象和实现可以独立扩展,不会相互影响,增强了系统的灵活性。
- 桥接模式的核心思想是将两个维度的变化分离:一个是抽象部分的变化,另一个是实现部分的变化。这种模式特别适合在需要跨多个平台或具有多个接口实现的场景中应用。
// 实现部分:颜色接口
class Color {
constructor(name) {
this.name = name;
}
applyColor() {
console.log(`Applying color: ${this.name}`);
}
}
// 具体实现:红色和蓝色
class Red extends Color {
constructor() {
super('red');
}
}
class Blue extends Color {
constructor() {
super('blue');
}
}
// 抽象部分:形状类
class Shape {
constructor(color) {
this.color = color; // 颜色是通过桥接引用的
}
draw() {
throw new Error('This method should be overwritten!');
}
}
// 扩展的抽象:不同的形状
class Circle extends Shape {
constructor(color) {
super(color);
}
draw() {
console.log('Drawing a Circle');
this.color.applyColor(); // 使用桥接引用的颜色
}
}
class Square extends Shape {
constructor(color) {
super(color);
}
draw() {
console.log('Drawing a Square');
this.color.applyColor(); // 使用桥接引用的颜色
}
}
// 使用桥接模式
const redCircle = new Circle(new Red());
const blueSquare = new Square(new Blue());
redCircle.draw(); // 输出:Drawing a Circle, Applying color: red
blueSquare.draw(); // 输出:Drawing a Square, Applying color: blue
关键点:
- 抽象部分:Shape 类定义了形状的基本功能,具体形状通过继承扩展。
- 实现部分:Color 类负责定义颜色的接口,不同的颜色通过继承实现。
- 桥接:形状通过 this.color 来桥接颜色,调用不同的颜色实现。
优势:
- 解耦:抽象与实现可以独立扩展,降低了系统的复杂度和耦合性。
- 可扩展性强:在不修改现有代码的情况下,轻松添加新的实现或抽象类。
- 减少代码重复:避免在不同的类中重复实现相似的功能(如颜色)。
劣势: - 复杂性增加:引入多个抽象类和接口后,系统结构可能变得更加复杂。
- 性能开销:使用桥接模式可能增加一些额外的间接调用,导致性能上的轻微开销。
- 平台无关的代码:当一个系统需要在多个平台上运行时,桥接模式可以帮助将平台相关的实现与平台无关的部分分离。
- 多维度变化的系统:当一个系统有多个维度的变化时(如形状和颜色、设备和操作系统),桥接模式可以帮助解耦这两个维度的变化。
- 图形绘制系统:如上例中,不同形状和颜色的组合是桥接模式的经典应用场景。
组合模式
- 组合模式(Composite Pattern)是一种结构型设计模式,它将对象组合成树形结构,用来表示部分-整体的层次结构。组合模式可以使客户端对单个对象和组合对象的使用具有一致性,这样可以在树结构中对单个对象和组合对象统一处理
- 组合模式在处理树形结构的数据时非常有用,比如文件系统中的文件和文件夹结构,组织架构中的员工和部门结构等。
// 抽象组件
class FileSystemComponent {
constructor(name) {
this.name = name;
}
// 组件的公共接口
display() {
throw new Error('This method should be overwritten!');
}
}
// 叶子节点:文件
class File extends FileSystemComponent {
constructor(name) {
super(name);
}
display() {
console.log(`File: ${this.name}`);
}
}
// 组合节点:文件夹
class Folder extends FileSystemComponent {
constructor(name) {
super(name);
this.children = []; // 存放子组件(文件或文件夹)
}
// 添加子组件
add(component) {
this.children.push(component);
}
// 移除子组件
remove(component) {
this.children = this.children.filter(child => child !== component);
}
// 展示文件夹及其子组件
display() {
console.log(`Folder: ${this.name}`);
this.children.forEach(child => child.display());
}
}
// 创建文件和文件夹
const file1 = new File('file1.txt');
const file2 = new File('file2.txt');
const file3 = new File('file3.txt');
const folder1 = new Folder('Folder 1');
const folder2 = new Folder('Folder 2');
// 组装文件夹结构
folder1.add(file1);
folder1.add(file2);
folder2.add(file3);
folder2.add(folder1);
// 展示整个文件系统
folder2.display();
// 输出:
// Folder: Folder 2
// File: file3.txt
// Folder: Folder 1
// File: file1.txt
// File: file2.txt
关键点:
- 叶子节点:File 类代表文件,是树结构的最小单位,不能包含其他组件。
- 组合节点:Folder 类代表文件夹,可以包含文件或其他文件夹,展示时递归显示其包含的子组件。
- 统一接口:无论是文件还是文件夹,都实现了 display() 方法,客户端可以用统一的方式处理它们。
优势:
- 简化客户端代码:客户端可以使用统一的方式处理所有对象,无论它们是单个对象还是组合对象,简化了代码逻辑。
- 符合开闭原则:可以轻松地添加新的叶子节点或组合节点,而无需修改现有代码。
- 递归结构:组合模式的树形结构非常适合递归操作,可以轻松地处理层次结构。
劣势: - 复杂性增加:当树的结构过于复杂时,组合模式可能引入更多的复杂性,管理树结构的操作会变得复杂。
- 类型安全问题:由于组合模式将叶子和组合对象放在一起,可能导致某些操作缺少类型安全的保证。
- 树形结构的数据:比如文件系统、组织架构、菜单系统等,使用组合模式可以方便地构建和操作这些层次结构。
- 图形编辑器:在图形编辑器中,可以将基本图形(如线条、矩形、圆等)组合成复杂的图形,使用组合模式可以统一管理这些图形。
- UI 组件树:很多 UI 框架(如 React、Vue 等)内部都使用了组合模式来管理组件树。
命令模式
- 命令模式(Command Pattern)是一种行为型设计模式,它将请求封装为对象,从而让我们可以用不同的请求对客户进行参数化。命令模式的核心思想是将请求的发送者与请求的接收者解耦,使得请求可以被储存、延迟执行、撤销等。
- 在命令模式中,命令是一个对象,它封装了具体的操作细节。这种模式特别适合事务处理、撤销/重做操作、宏命令等场景。
// 接收者:电灯
class Light {
on() {
console.log('The light is on');
}
off() {
console.log('The light is off');
}
}
// 命令对象接口
class Command {
execute() {
throw new Error('This method should be overwritten!');
}
undo() {
throw new Error('This method should be overwritten!');
}
}
// 具体命令对象:打开灯的命令
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.on();
}
undo() {
this.light.off();
}
}
// 具体命令对象:关闭灯的命令
class LightOffCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.off();
}
undo() {
this.light.on();
}
}
// 调用者:遥控器
class RemoteControl {
constructor() {
this.command = null;
}
setCommand(command) {
this.command = command;
}
pressButton() {
this.command.execute();
}
pressUndo() {
this.command.undo();
}
}
// 使用命令模式
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const lightOffCommand = new LightOffCommand(light);
const remoteControl = new RemoteControl();
// 打开灯
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton(); // 输出:The light is on
remoteControl.pressUndo(); // 输出:The light is off
// 关闭灯
remoteControl.setCommand(lightOffCommand);
remoteControl.pressButton(); // 输出:The light is off
remoteControl.pressUndo(); // 输出:The light is on
优势:
- 解耦性:调用者与接收者之间完全解耦,易于扩展和维护。
- 扩展性好:可以轻松添加新命令,而不需要修改调用者的代码。
- 支持撤销操作:通过 undo 方法实现撤销,增强了用户体验。
- 支持宏命令:可以将多个命令组合成一个宏命令,执行一系列操作。
劣势: - 增加了类的数量:每个命令都需要一个独立的类,这可能会增加代码的复杂性。
- 调试难度增加:命令模式将执行逻辑分散到多个命令对象中,可能增加调试的复杂性。
- 事务处理:命令模式适合实现复杂的事务操作,可以将事务拆分为多个命令,支持撤销和重做功能。
- 操作历史:在需要记录用户操作历史的场景中(如文本编辑器),命令模式非常适合实现撤销和重做功能。
- 宏命令:在需要一系列操作按特定顺序执行的场景中,可以将多个命令组合为一个宏命令。
- 远程控制系统:命令模式常用于控制家电、机器人等远程设备,命令对象可以代表设备的各种操作。
模板方法模式
- 模板方法模式(Template Method Pattern)是一种行为型设计模式,它定义了一个操作的骨架,将一些步骤的实现延迟到子类中。模板方法模式允许子类在不改变算法结构的前提下重新定义算法的某些步骤。
- 模板方法模式的核心思想是:将算法的通用部分放在父类中,而将具体步骤的实现留给子类。这保证了算法的整体结构不变,同时允许子类定制化某些细节。
// 抽象类:饮料
class Beverage {
prepare() {
this.boilWater();
this.brew(); // 留给子类实现
this.addCondiments(); // 留给子类实现
this.stir();
}
boilWater() {
console.log('Boiling water...');
}
stir() {
console.log('Stirring the drink...');
}
brew() {
throw new Error('This method must be overridden!');
}
addCondiments() {
throw new Error('This method must be overridden!');
}
}
// 具体类:咖啡
class Coffee extends Beverage {
brew() {
console.log('Brewing coffee...');
}
addCondiments() {
console.log('Adding sugar and milk...');
}
}
// 具体类:茶
class Tea extends Beverage {
brew() {
console.log('Steeping the tea...');
}
addCondiments() {
console.log('Adding lemon...');
}
}
// 使用模板方法模式制作饮料
const coffee = new Coffee();
coffee.prepare();
// 输出:
// Boiling water...
// Brewing coffee...
// Adding sugar and milk...
// Stirring the drink...
const tea = new Tea();
tea.prepare();
// 输出:
// Boiling water...
// Steeping the tea...
// Adding lemon...
// Stirring the drink...
关键点:
- 抽象类:Beverage 类中定义了通用的制作流程 prepare(),并且封装了 boilWater() 和 stir() 这两个固定的步骤。brew() 和 addCondiments() 方法由子类实现。
- 具体类:Coffee 和 Tea 类分别实现了 brew() 和 addCondiments() 方法,用于制作咖啡和茶时的具体步骤。
- 模板方法:prepare() 方法在抽象类中定义,它是算法的骨架,规定了饮料的制作顺序,具体步骤由子类负责。
优势:
- 避免重复代码:通用逻辑在父类中定义,子类只需实现定制化的部分,减少了代码重复。
- 易于维护:算法的整体结构固定在父类中,任何对算法的修改只需在父类中完成,易于维护。
- 符合开闭原则:通过继承和重写来扩展某些步骤,不需要修改模板方法,符合开闭原则。
劣势: - 子类行为受限:由于模板方法定义了算法的整体结构,子类只能按父类规定的顺序来定制某些步骤,灵活性可能受限。
- 增加类的数量:每个具体实现可能都需要创建一个新的子类,导致类的数量增加,代码结构变得复杂。
- 算法步骤固定,部分步骤可定制化:当某个操作的步骤是固定的,但某些步骤可以根据具体实现不同而变化时,模板方法模式非常合适。例如:各种饮料的制作、文件解析过程、游戏中的关卡生成等。
- 框架与应用:很多框架中会提供模板方法,应用程序可以在不改变框架核心功能的情况下,定制具体行为。例如,前端的生命周期钩子函数可以看作模板方法模式的应用。
- 钩子方法:模板方法模式常常会结合钩子方法一起使用,钩子方法是父类中定义的空方法,子类可以选择性地实现它们。
迭代器模式
- 迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种方法,能够顺序访问一个聚合对象中的各个元素,而不需要暴露其内部的表示。在 JavaScript 中,迭代器模式广泛用于对集合、数组、对象等进行遍历操作。
- 迭代器模式的核心思想是:将遍历操作与集合对象解耦,使得遍历逻辑可以独立于集合的实现而变化。
// 自定义迭代器
class NumberIterator {
constructor(numbers) {
this.numbers = numbers;
this.index = 0;
}
// next 方法用于获取集合中的下一个元素
next() {
if (this.index < this.numbers.length) {
return { value: this.numbers[this.index++], done: false };
} else {
return { value: undefined, done: true };
}
}
}
const numbers = [1, 2, 3, 4, 5];
const iterator = new NumberIterator(numbers);
// 使用迭代器遍历数组
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
// 输出:1 2 3 4 5
职责链模式
- 职责链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它允许将请求的发送者和接收者解耦,使得多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
- 职责链模式的核心思想是:将请求的发送者和接收者解耦,通过一条链路将请求传递给多个处理者,从而提高了代码的灵活性和可扩展性。
// 处理者抽象类
class Handler {
constructor(successor = null) {
this.successor = successor; // 下一个处理者
}
handleRequest(request) {
if (this.successor) {
this.successor.handleRequest(request); // 传递请求给下一个处理者
}
}
}
// 具体处理者类:经理
class Manager extends Handler {
handleRequest(request) {
if (request === 'leave' || request === 'overtime') {
console.log('Manager approved the request: ' + request);
} else {
super.handleRequest(request); // 继续传递请求
}
}
}
// 具体处理者类:总监
class Director extends Handler {
handleRequest(request) {
if (request === 'budget') {
console.log('Director approved the request: ' + request);
} else {
super.handleRequest(request); // 继续传递请求
}
}
}
// 具体处理者类:CEO
class CEO extends Handler {
handleRequest(request) {
console.log('CEO approved the request: ' + request);
}
}
// 创建职责链
const ceo = new CEO();
const director = new Director(ceo);
const manager = new Manager(director);
// 客户端发起请求
manager.handleRequest('leave'); // 输出: Manager approved the request: leave
manager.handleRequest('budget'); // 输出: Director approved the request: budget
manager.handleRequest('overtime'); // 输出: Manager approved the request: overtime
manager.handleRequest('other'); // 输出: CEO approved the request: other
关键点:
- Handler 抽象类:定义了处理请求的方法,并持有对下一个处理者的引用。具体的处理逻辑在子类中实现。
- 具体处理者类:Manager、Director 和 CEO 分别处理不同类型的请求。每个处理者都决定是否处理请求或者将请求传递给下一个处理者。
- 客户端:客户端通过第一个处理者(在此为 Manager)发起请求,依次传递给下一个处理者,直到请求被处理。
优势:
- 解耦合:请求发送者与处理者之间的解耦,降低了系统的复杂性。
- 可扩展性:可以轻松添加新的处理者,而不需要修改现有代码。
- 动态处理:根据请求的类型和处理者的能力动态处理请求。
劣势: - 调试复杂性:请求的处理可能经过多个处理者,调试时可能会比较复杂,不易追踪请求的处理路径。
- 性能开销:如果职责链很长,每个请求都需要经过多个处理者,可能导致性能问题,尤其是在频繁的请求场景中。
- 事件处理:在事件处理系统中,不同的事件处理器可以根据事件类型处理相应的请求,未处理的事件可以传递给下一个处理器。
- 日志处理:日志系统可以设计为责任链模式,日志请求可以通过不同的处理者(如控制台、文件、网络)传递,直到被适当的处理者处理。
- 审批系统:在工作流中,审批请求可以根据请求类型和权限等级传递给不同的审批者进行处理。