JavaScript与23种设计模式
创建型模式 (Creational Patterns)
1. 单例模式 (Singleton Pattern)
单例模式 (Singleton Pattern) 是一种创建型设计模式,其目的是确保在应用程序中只有一个实例对象存在,并提供对该实例的全局访问点。
实现单例模式的核心思想是通过将类的构造函数私有化,从而禁止外部代码直接实例化该类。同时,类还必须提供一个静态方法来返回该类的唯一实例。
以下是单例模式的示例代码:
class Singleton {
static instance = null; // 静态属性,用于保存唯一实例
constructor(name) {
if (Singleton.instance !== null) {
return Singleton.instance; // 如果实例已存在,则返回该实例
}
this.name = name;
Singleton.instance = this; // 将当前实例保存到静态属性中
}
static getInstance(name) {
return new Singleton(name); // 获取唯一实例的静态方法
}
}
// 示例使用
const instance1 = Singleton.getInstance('instance1');
const instance2 = Singleton.getInstance('instance2');
console.log(instance1 === instance2); // true,实例相同
console.log(instance1.name); // 'instance1'
console.log(instance2.name); // 'instance1',同一个实例
在上面的示例代码中,我们通过将 Singleton 类的构造函数私有化,从而禁止外部代码直接实例化该类。然后,我们通过静态属性 instance 来保存唯一实例,并在静态方法 getInstance 中获取该实例。在调用 getInstance 方法时,如果实例已存在,则直接返回该实例;否则,创建一个新的实例并将其保存到静态属性中。
使用单例模式可以确保在应用程序中只有一个实例对象存在,并提供对该实例的全局访问点。这在某些情况下非常有用,例如管理共享资源或配置信息等。
2. 工厂方法模式 (Factory Method Pattern)
工厂方法模式是一种创建型设计模式,它的主要目的是将对象的创建与使用代码分离开来,从而提高代码的可维护性、可扩展性和可重用性。
在实际开发中,我们经常需要根据不同的条件或参数创建不同类型的对象,在这种情况下,如果直接在客户端代码中进行对象的创建,会导致代码的耦合度较高,难以维护和扩展。因此,使用工厂方法模式可以将对象的创建交给具体的工厂类来完成,从而降低客户端代码与具体实现之间的耦合度。
下面是一个简单的 JavaScript 代码示例,演示如何使用工厂方法模式来创建不同类型的对象:
// 抽象工厂类
class AnimalFactory {
createAnimal() {
throw new Error('createAnimal must be implemented by subclass');
}
}
// 具体工厂类1
class DogFactory extends AnimalFactory {
createAnimal() {
return new Dog();
}
}
// 具体工厂类2
class CatFactory extends AnimalFactory {
createAnimal() {
return new Cat();
}
}
// 抽象产品类
class Animal {
speak() {
throw new Error('speak must be implemented by subclass');
}
}
// 具体产品类1
class Dog extends Animal {
speak() {
console.log('Woof!');
}
}
// 具体产品类2
class Cat extends Animal {
speak() {
console.log('Meow!');
}
}
// 客户端代码
const dogFactory = new DogFactory();
const dog = dogFactory.createAnimal();
dog.speak(); // output: 'Woof!'
const catFactory = new CatFactory();
const cat = catFactory.createAnimal();
cat.speak(); // output: 'Meow!'
在上面的代码示例中,我们通过抽象工厂类 AnimalFactory 和具体工厂类 DogFactory 和 CatFactory 来实现对象的创建,并将客户端代码与具体实现解耦。同时,我们也定义了抽象产品类 Animal 和具体产品类 Dog 和 Cat 来表示不同类型的动物,并通过重写 speak 方法来实现不同类型动物的叫声。最后,在客户端代码中,我们只需要根据需要选择具体的工厂类来创建不同类型的动物即可。
使用工厂方法模式可以使代码更加易于维护和扩展,并且可以降低客户端代码与具体实现之间的耦合度,提高代码的可重用性。
3. 抽象工厂模式 (Abstract Factory Pattern)
抽象工厂模式是一种创建型设计模式,它的主要目的是提供一个接口用于创建一组相关或相互依赖的对象,而无需指定它们的具体类。
该模式的设计目的是将客户端代码与具体实现解耦,从而使得客户端代码可以独立于具体实现变化。在实际开发中,我们经常需要创建一组相关或相互依赖的对象,并且这些对象通常会随着业务需求不断变化,因此使用抽象工厂模式可以更加灵活地处理这种情况。
下面是一个简单的 JavaScript 代码示例,演示如何使用抽象工厂模式来创建一组相关的对象:
// 抽象工厂类
class AbstractFactory {
createProductA() {
throw new Error('createProductA must be implemented by subclass');
}
createProductB() {
throw new Error('createProductB must be implemented by subclass');
}
}
// 具体工厂类1
class ConcreteFactory1 extends AbstractFactory {
createProductA() {
return new ConcreteProductA1();
}
createProductB() {
return new ConcreteProductB1();
}
}
// 具体工厂类2
class ConcreteFactory2 extends AbstractFactory {
createProductA() {
return new ConcreteProductA2();
}
createProductB() {
return new ConcreteProductB2();
}
}
// 抽象产品类A
class AbstractProductA {
methodA() {
throw new Error('methodA must be implemented by subclass');
}
}
// 具体产品类A1
class ConcreteProductA1 extends AbstractProductA {
methodA() {
console.log('Calling methodA of ConcreteProductA1');
}
}
// 具体产品类A2
class ConcreteProductA2 extends AbstractProductA {
methodA() {
console.log('Calling methodA of ConcreteProductA2');
}
}
// 抽象产品类B
class AbstractProductB {
methodB() {
throw new Error('methodB must be implemented by subclass');
}
}
// 具体产品类B1
class ConcreteProductB1 extends AbstractProductB {
methodB() {
console.log('Calling methodB of ConcreteProductB1');
}
}
// 具体产品类B2
class ConcreteProductB2 extends AbstractProductB {
methodB() {
console.log('Calling methodB of ConcreteProductB2');
}
}
// 客户端代码
const factory1 = new ConcreteFactory1();
const productA1 = factory1.createProductA();
productA1.methodA(); // output: 'Calling methodA of ConcreteProductA1'
const productB1 = factory1.createProductB();
productB1.methodB(); // output: 'Calling methodB of ConcreteProductB1'
const factory2 = new ConcreteFactory2();
const productA2 = factory2.createProductA();
productA2.methodA(); // output: 'Calling methodA of ConcreteProductA2'
const productB2 = factory2.createProductB();
productB2.methodB(); // output: 'Calling methodB of ConcreteProductB2'
在上面的代码示例中,我们通过抽象工厂类 AbstractFactory 和具体工厂类 ConcreteFactory1 和 ConcreteFactory2 来实现一组相关的对象的创建,并将客户端代码与具体实现解耦。同时,我们也定义了抽象产品类 AbstractProductA 和 AbstractProductB 和具体产品类 ConcreteProductA1、ConcreteProductA2、ConcreteProductB1 和 ConcreteProductB2 表示不同类型的产品,并通过重写对应的方法来实现不同类型产品的功能。最后,在客户端代码中,我们只需要根据需要选择具体的工厂类来创建一组相关的产品即可。
使用抽象工厂模式可以使代码更加易于维护和扩展,并且可以降低客户端代码与具体实现之间的耦合度,提高代码的可重用性。
4. 建造者模式 (Builder Pattern)
建造者模式是一种创建型设计模式,它的主要目的是将一个复杂对象的构建过程与其表示分离开来,从而使得同样的构建过程可以创建不同的表示。
该模式的设计目的是提高代码的可维护性和可扩展性,同时也能够降低客户端代码与具体实现之间的耦合度。在实际开发中,我们经常需要构建一些复杂的对象,并且这些对象的构建过程通常需要进行多个步骤的操作,因此使用建造者模式可以更加灵活地处理这种情况。
下面是一个简单的 JavaScript 代码示例,演示如何使用建造者模式来构建一个复杂对象:
// 建造者类
class Builder {
constructor() {
if (new.target === Builder) {
throw new Error('Builder is an abstract class and cannot be instantiated directly.');
}
}
buildPart1() {
throw new Error('buildPart1 must be implemented by subclass.');
}
buildPart2() {
throw new Error('buildPart2 must be implemented by subclass.');
}
getResult() {
throw new Error('getResult must be implemented by subclass.');
}
}
// 具体建造者类1
class ConcreteBuilder1 extends Builder {
constructor() {
super();
this.product = new Product();
}
buildPart1() {
this.product.part1 = 'Part1 from ConcreteBuilder1';
}
buildPart2() {
this.product.part2 = 'Part2 from ConcreteBuilder1';
}
getResult() {
return this.product;
}
}
// 具体建造者类2
class ConcreteBuilder2 extends Builder {
constructor() {
super();
this.product = new Product();
}
buildPart1() {
this.product.part1 = 'Part1 from ConcreteBuilder2';
}
buildPart2() {
this.product.part2 = 'Part2 from ConcreteBuilder2';
}
getResult() {
return this.product;
}
}
// 产品类
class Product {
constructor() {
this.part1 = '';
this.part2 = '';
}
print() {
console.log(`Part1: ${this.part1}`);
console.log(`Part2: ${this.part2}`);
}
}
// 指挥者类
class Director {
constructor(builder) {
this.builder = builder;
}
construct() {
this.builder.buildPart1();
this.builder.buildPart2();
return this.builder.getResult();
}
}
// 客户端代码
const builder1 = new ConcreteBuilder1();
const director1 = new Director(builder1);
const product1 = director1.construct();
product1.print(); // output: 'Part1: Part1 from ConcreteBuilder1', 'Part2: Part2 from ConcreteBuilder1'
const builder2 = new ConcreteBuilder2();
const director2 = new Director(builder2);
const product2 = director2.construct();
product2.print(); // output: 'Part1: Part1 from ConcreteBuilder2', 'Part2: Part2 from ConcreteBuilder2'
在上面的代码示例中,我们通过建造者类 Builder 和具体建造者类 ConcreteBuilder1 和 ConcreteBuilder2 来实现复杂对象的构建,并将客户端代码与具体实现解耦。同时,我们也定义了产品类 Product 来表示不同类型的产品,并通过重写 print 方法来实现不同类型产品的展示。最后,在客户端代码中,我们只需要根据需要选择具体的建造者类来构建不同类型的产品即可。
使用建造者模式可以使代码更加易于维护和扩展,并且可以降低客户端代码与具体实现之间的耦合度,提高代码的可重用性。
5. 原型模式 (Prototype Pattern)
原型模式是一种创建型设计模式,它的主要目的是使用已有对象的属性来创建新的对象,而无需显式地指定其具体类。
该模式的设计目的是提高代码的可维护性和可扩展性,同时也能够降低客户端代码与具体实现之间的耦合度。在实际开发中,我们经常需要创建多个相似但不完全相同的对象,并且这些对象通常会随着业务需求不断变化,因此使用原型模式可以更加灵活地处理这种情况。
下面是一个简单的 JavaScript 代码示例,演示如何使用原型模式来创建新的对象:
// 原型类
class Prototype {
clone() {
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
}
}
// 具体原型类1
class ConcretePrototype1 extends Prototype {
constructor() {
super();
this.property1 = 'Property1 from ConcretePrototype1';
this.property2 = 'Property2 from ConcretePrototype1';
}
}
// 具体原型类2
class ConcretePrototype2 extends Prototype {
constructor() {
super();
this.property1 = 'Property1 from ConcretePrototype2';
this.property2 = 'Property2 from ConcretePrototype2';
}
}
// 客户端代码
const prototype1 = new ConcretePrototype1();
const clonedPrototype1 = prototype1.clone();
console.log(clonedPrototype1.property1); // output: 'Property1 from ConcretePrototype1'
console.log(clonedPrototype1.property2); // output: 'Property2 from ConcretePrototype1'
const prototype2 = new ConcretePrototype2();
const clonedPrototype2 = prototype2.clone();
console.log(clonedPrototype2.property1); // output: 'Property1 from ConcretePrototype2'
console.log(clonedPrototype2.property2); // output: 'Property2 from ConcretePrototype2'
在上面的代码示例中,我们通过原型类 Prototype 和具体原型类 ConcretePrototype1 和 ConcretePrototype2 来实现新对象的创建,并将客户端代码与具体实现解耦。同时,我们也定义了不同类型的属性来表示不同类型的对象,并通过重写 clone 方法来实现新对象的创建。最后,在客户端代码中,我们只需要调用对应的 clone 方法即可创建新的对象。
使用原型模式可以使代码更加易于维护和扩展,并且可以降低客户端代码与具体实现之间的耦合度,提高代码的可重用性。
结构型模式 (Structural Patterns)
6. 适配器模式 (Adapter Pattern)
适配器模式 (Adapter Pattern) 是一种结构型设计模式,它允许不兼容接口的对象进行合作。
设计目的: 在应用程序开发中,经常会遇到需要集成不同组件或服务的情况。然而这些组件或服务的接口通常都不相同,无法直接协作。适配器模式的设计目的就是为了解决这个问题,它可以将一个接口转换成客户端所期望的另一个接口。
重构代码: 使用适配器模式可以提高代码的可复用性、可扩展性和可维护性,同时也可以降低系统的耦合度。
下面是一个简单的示例,假设有两个类,分别实现了不同的接口:
// Target interface
class Target {
request() {}
}
// Adaptee interface
class Adaptee {
specificRequest() {}
}
// Adapts the Adaptee interface to the Target interface
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest();
}
}
在上面的示例中,我们创建了三个类:Target、Adaptee 和 Adapter。其中,Target 定义了客户端所期望的接口,Adaptee 提供了一个不兼容的接口,而 Adapter 则是通过继承 Target 类并包装 Adaptee 对象来实现接口转换的。
通过这种方式,客户端就可以使用适配器对象来调用 Adaptee 提供的不兼容接口,而无需修改其原有代码。这样就实现了系统之间的解耦,提高了代码的可维护性和可扩展性。
示例代码如下:
const adaptee = {
specificRequest() {
console.log('Adaptee.specificRequest called');
}
}
const adapter = new Adapter(adaptee);
adapter.request(); // Output: Adaptee.specificRequest called
在上面的示例中,我们首先创建了一个 Adaptee 类型的对象 adaptee,其具有一个不兼容的 specificRequest 接口。然后,我们创建了一个 Adapter 对象 adapter,它包装了 adaptee 对象,并将其转换为符合 Target 接口的形式。最后,我们调用 adapter 对象的 request 方法,从而成功调用了 Adaptee 对象的 specificRequest 方法,输出了一条日志。
总结: 适配器模式是一种常见的设计模式,它可以帮助我们解决不同组件或服务之间的接口不兼容问题,使得系统更加灵活、可维护和可扩展。在实际开发中,我们可以根据具体情况选择不同的适配器实现方式,例如类适配器、对象适配器等。
7. 桥接模式 (Bridge Pattern)
桥接模式 (Bridge Pattern) 是一种结构型设计模式,它主要是为了解决多层继承和子类爆炸的问题。
设计目的: 在软件开发中,经常会遇到需要同时考虑多个因素的情况。例如,需要将多个不同的数据源进行组合展示,或者需要支持多种操作系统和不同的应用程序。这些场景下,如果采用传统的继承方式来实现,可能会导致类的数量太多,难以维护和扩展。桥接模式的设计目的就是为了解决这个问题,它可以将一个类的抽象部分与其具体实现部分分离开来,从而可以独立地变化它们。
重构代码: 使用桥接模式可以提高代码的可复用性、可扩展性和可维护性,同时也可以降低系统的耦合度。
下面是一个简单的示例,假设有两个类,分别表示不同的颜色和形状:
// Implementor interface
class Color {
fill() {}
}
// Concrete Implementor
class RedColor extends Color {
fill() {
console.log('Red color filled');
}
}
class GreenColor extends Color {
fill() {
console.log('Green color filled');
}
}
// Abstraction interface
class Shape {
constructor(color) {
this.color = color;
}
draw() {}
}
// Refined Abstraction
class Circle extends Shape {
draw() {
console.log('Circle drawn with ');
this.color.fill();
}
}
class Rectangle extends Shape {
draw() {
console.log('Rectangle drawn with ');
this.color.fill();
}
}
在上面的示例中,我们创建了四个类:Color、RedColor、GreenColor 和 Shape、Circle、Rectangle。其中,Color 定义了所有颜色实现类所需遵循的接口,RedColor 和 GreenColor 是两个具体的颜色实现类。Shape 则定义了所有形状对象所需遵循的接口,而 Circle 和 Rectangle 则是两个具体的形状对象。
通过使用桥接模式,我们可以将 Shape 类和 Color 类分别抽象出来,从而将它们的变化分离开来。这样,我们就可以灵活地组合不同的形状对象和颜色对象,并且可以随时添加新的形状或颜色,而不需要修改原有代码。
示例代码如下:
const red = new RedColor();
const green = new GreenColor();
const circle = new Circle(red);
circle.draw(); // Output: Circle drawn with Red color filled
const rectangle = new Rectangle(green);
rectangle.draw(); // Output: Rectangle drawn with Green color filled
在上面的示例中,我们首先创建了两个颜色实现类 RedColor 和 GreenColor,然后创建了两个形状对象 Circle 和 Rectangle,并且将它们分别与不同的颜色对象组合起来。最后,我们调用了 draw 方法,从而成功绘制了两个不同的形状,并且使用了不同的颜色。
总结: 桥接模式是一种常见的设计模式,它可以帮助我们解决多层继承和子类爆炸的问题,提高代码的可维护性和可扩展性。在实际开发中,我们可以根据具体情况选择不同的桥接实现方式,例如类桥接、对象桥接等。
8. 组合模式 (Composite Pattern)
组合模式 (Composite Pattern) 是一种结构型设计模式,它将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
设计目的: 在应用程序开发中,我们经常需要处理复杂的对象和类之间的关系。这些关系可能会导致系统的耦合度过高,从而使得系统难以维护和扩展。组合模式的设计目的就是为了解决这个问题,它可以让客户端以统一的方式处理单个对象和组合对象,从而降低系统的耦合度。
重构代码: 使用组合模式可以提高代码的可复用性、可扩展性和可维护性,同时也可以降低系统的耦合度。
下面是一个简单的示例,假设我们要处理某个文件夹中的文件或者子文件夹:
class File {
constructor(name) {
this.name = name;
}
display() {
console.log(this.name);
}
}
class Folder {
constructor(name) {
this.name = name;
this.children = [];
}
add(fileOrFolder) {
this.children.push(fileOrFolder);
}
remove(fileOrFolder) {
const index = this.children.indexOf(fileOrFolder);
if (index !== -1) {
this.children.splice(index, 1);
}
}
display() {
console.log(this.name);
for (const child of this.children) {
child.display();
}
}
}
在上面的示例中,我们创建了两个类:File 和 Folder。其中,File 表示文件,Folder 表示文件夹。Folder 类中包含一个数组 children,用于存储其包含的文件或子文件夹。
使用组合模式,客户端可以以统一的方式处理单个对象和组合对象。例如,如果需要显示某个文件夹中所有的文件和子文件夹,只需要调用该文件夹的 display 方法即可。该方法会遍历所有子节点,并递归调用它们的 display 方法。
示例代码如下:
const folder = new Folder('My Documents');
const file1 = new File('resume.docx');
const file2 = new File('photo.jpg');
const subFolder = new Folder('Work');
const file3 = new File('project.pptx');
subFolder.add(file3);
folder.add(file1);
folder.add(file2);
folder.add(subFolder);
folder.display();
在上面的示例中,我们首先创建了一个名为 My Documents 的文件夹 folder,然后依次创建了两个文件和一个名为 Work 的子文件夹 subFolder,并将它们添加到 folder 中。最后,我们调用 folder 的 display 方法来输出该文件夹及其包含的所有文件和子文件夹的名称。
总结: 组合模式是一种常见的设计模式,它可以帮助我们处理复杂的对象和类之间的关系,从而降低系统的耦合度。在实际开发中,我们可以根据具体情况选择不同的组合方式,例如以文件夹为单位进行组合、以部门为单位进行组合等。
9. 装饰者模式 (Decorator Pattern)
装饰者模式的设计目的是在不改变对象原有结构的情况下,动态地为对象添加额外的行为或责任。这种设计模式可以在运行时动态地扩展一个对象的功能。
具体实现: 装饰者模式通过将原始对象作为参数传递给装饰器来实现。装饰器函数通常与原始函数具有相同的接口,同时还接收一个指向原始函数的引用。这样,当客户端调用装饰器函数时,装饰器函数会将请求转发给原始函数,并可以在请求前后添加额外的行为。
以下是一个简短的 JavaScript 示例:
function component() {
return 'Component';
}
function decoratorA(fn) {
return function() {
return `Decorator A(${fn()})`;
}
}
function decoratorB(fn) {
return function() {
return `Decorator B(${fn()})`;
}
}
// 使用
const decorated = decoratorB(decoratorA(component));
console.log(decorated()); // 输出 "Decorator B(Decorator A(Component))"
在这个示例中,component 是原始的函数,代表一份组件。decoratorA 和 decoratorB 分别是两个装饰器函数,它们都接收一个指向原始函数的引用,并在其前后添加了相应的内容。
通过这个示例,我们可以看到装饰者模式如何通过将原始函数作为参数传递给装饰器函数来实现动态扩展函数的功能。我们还可以轻松地为函数添加多个装饰器,从而进一步扩展其功能。
当然,在实际开发中,我们可能需要使用更复杂的数据结构来存储和管理装饰器函数,但是基本原理都是相同的。
10. 外观模式 (Facade Pattern)
外观模式的设计目的是提供一个简单的接口,隐藏较为复杂的子系统,并使其易于使用。这种设计模式可以为客户端提供一个统一的入口点,使得客户端不需要了解子系统的具体实现细节。
具体实现: 外观模式通过封装子系统中的一组接口来实现。外观函数包含对这些接口的引用,并将其暴露给客户端。当客户端调用外观函数时,外观函数会负责将请求转发给其中包含的子系统接口,并返回相应的结果。
以下是一个简短的 JavaScript 示例:
function subsystemA() {
return 'Subsystem A';
}
function subsystemB() {
return 'Subsystem B';
}
function subsystemC() {
return 'Subsystem C';
}
function facade() {
return `${subsystemA()} ${subsystemB()} ${subsystemC()}`;
}
// 使用
console.log(facade()); // 输出 "Subsystem A Subsystem B Subsystem C"
在这个示例中,subsystemA、subsystemB 和 subsystemC 分别代表三个子系统。facade 函数是外观函数,它封装了这三个子系统的接口,并提供了一个简单的统一入口点。
通过这个示例,我们可以看到外观模式如何通过封装一组复杂的子系统接口来提供一个更加简单易用的接口。我们可以将多个复杂的函数组合成一个更加简单的接口,从而降低客户端与子系统之间的耦合度。
11. 享元模式 (Flyweight Pattern)
享元模式的设计目的是通过共享对象来尽可能减少内存使用和对象创建成本。这种设计模式可以在系统中复用相似的对象,从而提高系统的性能和效率。
具体实现: 享元模式通过将相似的对象抽象成一个单独的对象,并在需要时共享它们来实现。享元函数通常包含一个内部状态和一个外部状态。内部状态是不变的,而外部状态则会随着客户端请求的不同而发生变化。享元函数可以接收客户端传递的外部状态,并在内部状态不变的情况下返回一个已经存在或者新创建的对象。
以下是一个简短的 JavaScript 示例:
function FlyweightFactory() {
const flyweights = {};
function getFlyweight(key) {
if (flyweights[key]) {
return flyweights[key];
} else {
const flyweight = {
operation: function(sharedState) {
console.log(`Flyweight(${key}): shared state is ${sharedState}`);
}
};
flyweights[key] = flyweight;
return flyweight;
}
}
return {
get: getFlyweight
};
}
// 使用
const factory = FlyweightFactory();
const flyweight1 = factory.get('A');
const flyweight2 = factory.get('B');
flyweight1.operation(1); // 输出 Flyweight(A): shared state is 1
flyweight2.operation(2); // 输出 Flyweight(B): shared state is 2
在这个示例中,FlyweightFactory 是一个享元工厂函数,用于创建和管理享元对象。在这个示例中,我们创建了两个不同的享元对象(A和B)。
当客户端需要获取一个享元对象时,可以通过调用工厂函数的 getFlyweight 方法来实现。如果工厂函数中已经存在相应的享元对象,则直接返回该对象;否则,新创建一个享元对象并添加到缓存中。
通过这个示例,我们可以看到享元模式如何通过共享相似对象来提高系统性能和效率。我们可以将相似的对象抽象成一个单独的对象,并将其缓存在内存中,从而减少内存使用和对象创建成本。
12. 代理模式 (Proxy Pattern)
代理模式的设计目的是在不改变原始对象的情况下控制对其访问。这种设计模式可以为客户端提供一个代理对象,从而控制和管理对原始对象的访问。
具体实现: 代理模式通过创建一个代理函数来间接访问原始对象。代理函数通常与原始函数具有相同的接口,并包含一个指向原始函数的引用。当客户端调用代理函数时,代理函数会将请求转发给原始函数,并可以在请求前后添加额外的逻辑。
以下是一个简短的 JavaScript 示例:
function subject() {
this.request = function() {
return 'Subject Request';
};
}
function proxy(subject) {
this.subject = subject;
this.request = function() {
if (this.checkAccess()) {
return this.subject.request();
} else {
return 'Proxy: Access denied';
}
};
this.checkAccess = function() {
// 检查用户是否有权限访问该对象
return true;
};
}
// 使用
const sub = new subject();
const pro = new proxy(sub);
console.log(pro.request()); // 输出 "Subject Request"
在这个示例中,subject 是原始对象,代表一份主题。proxy 函数是代理函数,它间接访问了原始对象并控制了对其的访问。
当客户端调用代理函数 request 时,代理函数会先检查用户是否有权限访问该对象,并根据结果决定是否将请求转发给原始对象。通过这个示例,我们可以看到代理模式如何通过一个代理函数来控制对原始对象的访问,并添加额外的逻辑。
在实际开发中,我们可能需要使用更复杂的数据结构和算法来实现代理模式,但基本原理都是相同的。
行为型模式 (Behavioral Patterns)
13. 职责链模式 (Chain of Responsibility Pattern)
职责链模式是一种行为型设计模式,它允许你将请求沿着处理者的链进行发送,直到有一个处理者能够处理该请求为止。在此过程中,请求通过链传递,每个处理者都可以选择处理该请求或将其传递给下一个处理者。这种模式可以帮助我们避免将请求的发送者与接收者耦合在一起,从而实现解耦。
下面是职责链模式的重构步骤:
- 创建一个处理请求的函数,其中包含多个处理请求的子函数。
- 每个子函数都需要判断当前是否能够处理请求,如果可以则处理请求并返回结果,否则将请求传递给下一个子函数。
- 在调用第一个子函数处发送请求。该处理函数会尝试处理请求,如果不能处理则会将请求传递给下一个子函数,以此类推,直到最后一个子函数。
以下是一个职责链模式的示例代码:
function handleRequest(request) {
function concreteHandler1(request) {
if (request === 'request type 1') {
return 'handled by ConcreteHandler1';
}
return null;
}
function concreteHandler2(request) {
if (request === 'request type 2') {
return 'handled by ConcreteHandler2';
}
return null;
}
function concreteHandler3(request) {
if (request === 'request type 3') {
return 'handled by ConcreteHandler3';
}
return null;
}
const handlers = [concreteHandler1, concreteHandler2, concreteHandler3];
for (const handler of handlers) {
const result = handler(request);
if (result !== null) {
return result;
}
}
return null;
}
console.log(handleRequest('request type 2')); // 输出:handled by ConcreteHandler2
console.log(handleRequest('request type 4')); // 输出:null
在上述示例中,我们创建了一个处理请求的函数 handleRequest ,其中包含多个子函数 concreteHandler1、concreteHandler2 和 concreteHandler3 。每个子函数都会尝试处理传入的请求,并返回结果。如果当前子函数不能处理请求,则将请求传递给下一个子函数,以此类推,直到有一个子函数能够处理该请求或者处理完所有子函数。
应用场景: 职责链模式适合以下情况:
- 当需要避免将请求发送者和接收者之间的耦合时,可以使用职责链模式。例如,我们可以让多个处理器独立地处理一个请求,从而使得请求的发送者和接收者不需要知道具体的处理过程。
- 当需要动态指定能够处理请求的对象集合时,职责链模式也是一个不错的选择。通过将多个处理器组成一个链,我们可以很方便地添加或移除处理器。
- 当需要按顺序执行一系列操作时,职责链模式同样适用。每个处理器可以代表一个步骤,并且它们可以根据需要对请求进行修改、拒绝或延迟处理。
14. 命令模式 (Command Pattern)
命令模式旨在将请求封装为对象,从而允许我们使用不同的请求、队列或日志来参数化其他对象。命令也支持可撤消操作。
该模式的设计目的是将询问者和命令执行者解耦,使得它们可以独立变化。实现该模式的方法是通过将函数调用转换为具有共同接口的单个类的实例。
在这里,我们将使用 canvas 绘制一个简单的矩形,并使用命令模式支持撤销操作。
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// 命令对象
const DrawRectangleCommand = {
execute: function(x, y, w, h) {
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.stroke();
},
undo: function() {
// 撤销清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
};
// 命令队列
const CommandQueue = {
commands: [],
addCommand: function(command) {
this.commands.push(command);
command.execute(...command.args);
},
undo: function() {
const command = this.commands.pop();
command.undo();
}
}
// 调度程序对象
const RemoteControl = {
submit: function(command) {
CommandQueue.addCommand(command);
},
undo: function() {
CommandQueue.undo();
}
};
// 使用
RemoteControl.submit({
...DrawRectangleCommand,
args: [50, 50, 100, 100]
});
// 撤销
document.getElementById("undoButton").addEventListener("click", function() {
RemoteControl.undo();
});
在这个示例中,我们创建了一个 DrawRectangleCommand 对象,它实现了 execute 和 undo 方法。当 execute 被调用时,它会在画布上绘制一个矩形。撤销操作会清除画布。
我们还创建了一个命令队列 CommandQueue,它允许我们添加新命令以及撤销最新的命令。当我们调用 RemoteControl.submit 方法时,实际上是将命令添加到 CommandQueue 中并立即执行。我们通过传递一个包含命令和参数的对象来支持参数化命令。
最后,我们监听一个按钮点击事件,以便用户可以撤销最近的命令。
总之,命令模式是一种非常有用的设计模式,它允许我们使用命令对象封装不同的功能,并支持可撤消操作。在 JavaScript 中,我们可以使用函数和对象来轻松实现该模式。
15. 解释器模式 (Interpreter Pattern)
解释器模式是一种行为型设计模式,它定义了一种语言,并且定义了一个解释器来执行该语言。其实现需要定义 AST(抽象语法树)来表示语言的语法结构,以及编写对应的解析器来解析和执行 AST。以下是一个简单的 JavaScript 代码示例:
// 定义语法规则的抽象语法树(AST)
class Expression {
interpret() {}
}
class NumberExpression extends Expression {
constructor(value) {
super();
this.value = value;
}
interpret() {
return this.value;
}
}
class PlusExpression extends Expression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
interpret() {
return this.left.interpret() + this.right.interpret();
}
}
// 编写解释器来解析和执行 AST
function evaluate(expression) {
return expression.interpret();
}
// 使用示例
const expression = new PlusExpression(
new NumberExpression(5),
new PlusExpression(
new NumberExpression(3),
new NumberExpression(2)
)
);
console.log(evaluate(expression)); // 输出 10
在上面的示例中,我们定义了三个 AST 节点类:Expression、NumberExpression 和 PlusExpression。其中 NumberExpression 表示数字,PlusExpression 表示加法表达式。每个节点类都实现了 interpret() 方法,用于执行对应的表达式。然后我们编写了一个 evaluate() 函数来解析和执行 AST,它接受一个 AST 根节点作为参数,并返回该节点表示的表达式的值。最后我们创建了一个加法表达式的 AST,通过 evaluate() 函数计算出其值并输出。
16. 迭代器模式 (Iterator Pattern)
迭代器模式是一种行为型设计模式,它允许我们按照特定方式遍历集合元素,而不暴露其底层表示。该模式可以通过提供一个统一的接口来实现对各种类型集合的迭代操作,从而使得客户端代码无需了解集合内部的结构细节。
在JavaScript 中,迭代器模式通常用于遍历数组或其他类似数据结构。以下是一个简单的例子,说明如何使用迭代器模式来遍历数组:
const myArray = [1, 2, 3, 4, 5];
function createIterator(array) {
let index = 0;
return {
next: function() {
return index < array.length ?
{ value: array[index++], done: false } :
{ done: true };
}
};
}
const iterator = createIterator(myArray);
while (true) {
const item = iterator.next();
if (item.done) {
break;
}
console.log(item.value);
}
在上面的示例中,我们首先定义了一个名为 createIterator 的函数,它接受一个数组作为输入,并返回一个包含 next 方法的对象,该方法每次调用都会返回下一个数组元素。next 方法返回一个对象,其中包含两个属性:value 表示当前元素的值,done 表示是否已经到达数组末尾。
然后,我们使用 createIterator 函数创建了一个针对 myArray 数组的迭代器。最后,我们使用 while 循环遍历整个数组,并输出每个元素的值。
使用迭代器模式可以将集合内部结构隐藏起来,同时提供了一种清晰简洁的方式来访问其中的元素。此外,通过将遍历操作委托给迭代器对象,可以使得代码更加灵活,例如可以在同一个集合上执行多个不同类型的遍历操作。
注意,ES6 中引入了 for..of 循环语句,它已经内置了对迭代器模式的支持,因此在实际开发中,可以直接使用该语句来遍历数组或其他可迭代对象。
17. 中介者模式 (Mediator Pattern)
中介者模式是一种行为型设计模式,它允许对象之间通过一个中介者对象进行通信,以避免这些对象之间直接耦合。该模式可以使得各个对象之间的通信更加简单,并且能够随着系统的扩展而保持灵活性。
在 JavaScript 中,中介者模式通常用于管理复杂的交互式应用程序,例如网页游戏或者用户界面。以下是一个简单的示例,说明如何使用中介者模式来协调多个对象之间的交互:
function Mediator() {
let components = [];
this.register = function(component) {
components.push(component);
component.setMediator(this);
};
this.notify = function(sender, event) {
for (let i = 0; i < components.length; i++) {
if (components[i] !== sender) {
components[i].receive(event);
}
}
};
}
function Component(name) {
this.name = name;
this.mediator = null;
this.setMediator = function(mediator) {
this.mediator = mediator;
};
this.send = function(event) {
console.log(`${this.name} sends ${event}.`);
this.mediator.notify(this, event);
};
this.receive = function(event) {
console.log(`${this.name} receives ${event}.`);
};
}
const mediator = new Mediator();
const component1 = new Component('Component 1');
const component2 = new Component('Component 2');
mediator.register(component1);
mediator.register(component2);
component1.send('Hello');
component2.send('World');
在上面的示例中,我们定义了一个 Mediator 类和一个 Component 类。Mediator 类负责管理多个 Component 对象,并提供一种通信机制,使得这些对象之间可以通过它进行交互。Component 类表示各个组件对象,每个对象都包含一个对中介者对象的引用。
我们先创建了一个 Mediator 实例,然后向其中注册了两个 Component 实例。之后,我们通过调用 send 方法来发送消息,在消息发送过程中,每个组件对象都将其消息发送给中介者,中介者再将其转发给其他组件对象。最终,所有组件对象都会收到相应的消息。
使用中介者模式可以降低对象之间的耦合度,从而提高系统的可维护性和可扩展性。此外,该模式还能够使得系统更加灵活,例如可以动态地添加或删除组件对象,而不需要修改现有的代码。
18. 备忘录模式 (Memento Pattern)
备忘录模式 (Memento Pattern) 是一种软件设计模式,它允许你将对象的状态保存到一个备忘录中,以便在需要时恢复该状态。备忘录模式的主要目的是提供一种回滚机制,以便恢复对象的旧状态。
在实际应用中,当我们需要撤销某些操作时,备忘录模式可以非常有用。例如,在文本编辑器中,用户可能会想要撤销之前的一些操作,比如删除一段文本或者修改一段文本。这时候,可以使用备忘录模式来存储编辑器的状态,并在需要时进行恢复。
下面是一个简单的使用备忘录模式的代码示例:
// 创建一个备忘录类,用于保存对象的状态
class Memento {
constructor(state) {
this._state = state;
}
getState() {
return this._state;
}
}
// 创建一个原始对象类,它包含需要保存的状态
class OriginalObject {
constructor() {
this._state = null;
}
setState(state) {
this._state = state;
}
getState() {
return this._state;
}
// 创建一个方法,用于创建备忘录并将当前状态保存到备忘录中
saveStateToMemento() {
return new Memento(this._state);
}
// 创建一个方法,用于从备忘录中恢复状态
restoreStateFromMemento(memento) {
this._state = memento.getState();
}
}
// 创建一个对象,设置状态并将其保存到备忘录中
const originalObject = new OriginalObject();
originalObject.setState('State 1');
const memento = originalObject.saveStateToMemento();
// 修改对象的状态,并将其保存到另一个备忘录中
originalObject.setState('State 2');
const newMemento = originalObject.saveStateToMemento();
// 将对象恢复到之前的状态
originalObject.restoreStateFromMemento(memento);
console.log(originalObject.getState()); // 输出 State 1
// 将对象恢复到最新的状态
originalObject.restoreStateFromMemento(newMemento);
console.log(originalObject.getState()); // 输出 State 2
在上面的示例中,我们创建了两个类:Memento和OriginalObject。Memento类用于保存原始对象的状态,而OriginalObject类包含要保存的状态以及用于保存和恢复状态的方法。
当我们需要保存对象的状态时,我们可以调用saveStateToMemento方法来创建一个备忘录并将当前状态保存到其中。同时,我们也可以使用getState方法来获取对象的当前状态。
当我们需要恢复对象的状态时,我们可以传递一个备忘录给restoreStateFromMemento方法,该方法将从备忘录中获取状态并将其还原到对象中。
总的来说,备忘录模式非常适合需要撤销操作或者回滚机制的场景。它可以帮助我们轻松地保存和恢复对象的状态,从而提高代码的可靠性和可维护性。
19. 观察者模式 (Observer Pattern)
观察者模式 (Observer Pattern) 是一种软件设计模式,它定义了一种一对多的依赖关系,使得当一个对象状态发生改变时,所有依赖于它的对象都会自动收到通知并更新。观察者模式的主要目的是解耦合作用,将观察者与被观察者分离开来,从而实现松散耦合。
在实际应用中,观察者模式非常有用。例如,在一个图形界面程序中,当用户点击按钮或者输入文本时,不同的组件需要做出相应的响应。这时候,可以使用观察者模式来实现组件之间的通信和协调。
下面是一个简单的使用观察者模式的代码示例:
// 创建一个被观察者对象
const observable = {
observers: [], // 存储观察者对象
// 注册观察者
addObserver(observer) {
this.observers.push(observer);
},
// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter(item => item !== observer);
},
// 通知所有观察者
notifyObservers() {
this.observers.forEach(observer => observer.update());
}
}
// 创建一个观察者对象
const observer = {
update() {
console.log('Observer has been updated');
}
}
// 注册观察者到被观察者中
observable.addObserver(observer);
// 通知所有观察者
observable.notifyObservers(); // 输出 Observer has been updated
// 移除观察者
observable.removeObserver(observer);
在上面的示例中,我们定义了一个被观察者对象observable和一个观察者对象observer。被观察者对象包含了一个observers数组,用于存储观察者对象。
当我们需要注册观察者时,可以调用addObserver方法将观察者添加到observers数组中。当我们需要移除观察者时,可以调用removeObserver方法将观察者从observers数组中移除。
当被观察者状态发生改变时,我们可以调用notifyObservers方法,它将遍历所有观察者并调用它们的update方法。
总的来说,观察者模式非常适合需要解耦合作用的场景。它可以帮助我们将观察者与被观察者分离开来,从而提高代码的可维护性和可扩展性。
20. 状态模式 (State Pattern)
状态模式 (State Pattern) 是一种软件设计模式,它允许对象在内部状态发生变化时改变其行为。状态模式的主要目的是将条件语句从业务逻辑中分离出来,提高代码的可读性和可维护性。
在实际应用中,状态模式非常有用。例如,在一个游戏中,玩家的能力和状态随着游戏进程而变化。这时候,可以使用状态模式来管理玩家的状态,并根据不同状态来调整游戏中的行为。
下面是一个简单的使用状态模式的代码示例:
// 定义状态接口
const State = {
attack() {},
defend() {},
escape() {}
};
// 创建具体状态:普通状态
const normalState = {
attack() {
console.log('Player is attacking with normal power');
},
defend() {
console.log('Player is defending with normal defense');
},
escape() {
console.log('Player is escaping with normal speed');
}
};
// 创建具体状态:强化状态
const enhanceState = {
attack() {
console.log('Player is attacking with enhanced power');
},
defend() {
console.log('Player is defending with enhanced defense');
},
escape() {
console.log('Player is escaping with enhanced speed');
}
};
// 创建对象并设置初始状态
let player = {
state: normalState,
// 定义方法:攻击
attack() {
this.state.attack();
},
// 定义方法:防御
defend() {
this.state.defend();
},
// 定义方法:逃跑
escape() {
this.state.escape();
},
// 定义方法:切换状态
changeState(newState) {
this.state = newState;
}
};
// 玩家使用普通状态
player.attack(); // 输出 Player is attacking with normal power
player.defend(); // 输出 Player is defending with normal defense
player.escape(); // 输出 Player is escaping with normal speed
// 玩家使用强化状态
player.changeState(enhanceState);
player.attack(); // 输出 Player is attacking with enhanced power
player.defend(); // 输出 Player is defending with enhanced defense
player.escape(); // 输出 Player is escaping with enhanced speed
在上面的示例中,我们定义了一个State接口,并创建了两个具体状态:normalState和enhanceState。每个具体状态实现了State接口中的方法,并提供了不同的行为。
我们还创建了一个player对象,并设置其初始状态为normalState。player对象包含了三个方法:attack、defend和escape,分别用于执行攻击、防御和逃跑操作。当我们需要切换玩家状态时,可以调用changeState方法来改变player对象的状态。
总的来说,状态模式非常适合需要管理复杂状态和行为的场景。它可以帮助我们将条件语句从业务逻辑中分离出来,从而提高代码的可读性和可维护性。
21. 策略模式 (Strategy Pattern)
策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使他们可以相互替换。策略模式让算法独立于使用它的客户端而变化。
策略模式的主要目的是将算法的定义与应用程序的使用分开,使得算法可以独立于其客户端进行修改和扩展。此外,策略模式还可以帮助减少代码重复,提高代码的可读性和可维护性。
下面是一个简单的JavaScript示例,展示如何使用策略模式来处理不同的支付方式:
// 定义具体的支付策略
const PaymentStrategies = {
'wechat': function(amount) {
console.log(`Pay ${amount} RMB by WeChat Pay`);
},
'alipay': function(amount) {
console.log(`Pay ${amount} RMB by AliPay`);
},
'creditcard': function(amount) {
console.log(`Pay ${amount} RMB by Credit Card`);
}
};
// 定义支付函数
function pay(order, paymentMethod) {
const amount = order.amount;
const paymentFunction = PaymentStrategies[paymentMethod];
if (paymentFunction) {
paymentFunction(amount);
} else {
console.error(`Unsupported payment method: ${paymentMethod}`);
}
}
// 测试不同的支付方式
const order1 = { amount: 100 };
pay(order1, 'wechat'); // 输出 "Pay 100 RMB by WeChat Pay"
const order2 = { amount: 200 };
pay(order2, 'alipay'); // 输出 "Pay 200 RMB by AliPay"
const order3 = { amount: 300 };
pay(order3, 'creditcard'); // 输出 "Pay 300 RMB by Credit Card"
const order4 = { amount: 400 };
pay(order4, 'paypal'); // 输出 "Unsupported payment method: paypal"
在上面的示例中,我们定义了不同的支付策略,并将它们作为对象的属性存储在PaymentStrategies对象中。当需要执行支付操作时,我们可以使用pay函数并传入订单信息和支付方式来选择相应的支付策略。如果指定的支付方式不被支持,则会输出错误信息。
总的来说,策略模式是一种非常有用的设计模式,可以帮助我们提高代码的可维护性和可扩展性,并让代码更加灵活和易于修改。
22. 模板方法模式 (Template Method Pattern)
模板方法模式是一种行为型设计模式,其目的是定义一个算法的框架,并将其中一些步骤延迟到子类中实现。这可以使得算法的框架不变,但具体步骤可由子类按需实现,以满足不同的需求。
在JavaScript中,我们可以使用函数来实现模板方法模式。以下是一个简单的例子:
function printHeader () {
console.log('--- start ---')
}
function printFooter () {
console.log('--- end ---')
}
function templateMethod(algorithm) {
printHeader();
algorithm.step1();
algorithm.step2();
algorithm.step3();
printFooter();
}
const concreteAlgorithmA = {
step1() {
console.log("Concrete Algorithm A - Step 1");
},
step2() {
console.log("Concrete Algorithm A - Step 2");
},
step3() {
console.log("Concrete Algorithm A - Step 3");
}
};
const concreteAlgorithmB = {
step1() {
console.log("Concrete Algorithm B - Step 1");
},
step2() {
console.log("Concrete Algorithm B - Step 2");
},
step3() {
console.log("Concrete Algorithm B - Step 3");
}
};
templateMethod(concreteAlgorithmA);
// Output:
// --- start ---
// Concrete Algorithm A - Step 1
// Concrete Algorithm A - Step 2
// Concrete Algorithm A - Step 3
// --- end ---
templateMethod(concreteAlgorithmB);
// Output:
// --- start ---
// Concrete Algorithm B - Step 1
// Concrete Algorithm B - Step 2
// Concrete Algorithm B - Step 3
// --- end ---
在上面的代码示例中,我们定义了一个 templateMethod 函数,它接受一个参数 algorithm,代表一个算法对象。算法对象必须实现 step1、step2 和 step3 方法。
我们还定义了两个具体算法对象 concreteAlgorithmA 和 concreteAlgorithmB,它们分别实现了自己的 step1、step2 和 step3 方法。然后,我们将这两个具体算法对象传递给 templateMethod 函数,使得它们都按照模板方法执行了相同的算法流程。
通过使用模板方法模式,我们可以将算法的框架从具体步骤中解耦出来,使得算法的核心流程更加清晰明了。同时,由于具体步骤被延迟到子类中实现,我们可以根据不同的需求灵活地创建新的算法对象。
23. 访问者模式 (Visitor Pattern)
访问者模式是一种行为型设计模式,其主要目的是将算法与对象结构分离开来,使得算法可以独立于对象结构而变化。该模式通过在不修改对象结构的情况下,在对象中添加新的操作方法,从而完成对对象结构的扩展。
在访问者模式中,有两个重要的角色:访问者和被访问元素。访问者定义了一个访问集合中所有元素的方法,而被访问元素则提供了一组接受访问者访问的方法。
以下是一个简单的JavaScript实现访问者模式的示例:
// 定义访问者
const visitor = {
// 访问 Number 类型元素
visitNumber: function(element) {
console.log(`Number: ${element}`);
},
// 访问 Boolean 类型元素
visitBoolean: function(element) {
console.log(`Boolean: ${element}`);
}
};
// 定义被访问元素
const elements = [1, true, "hello"];
// 定义访问函数
function visitElements(elements, visitor) {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (typeof element === "number") {
visitor.visitNumber(element);
} else if (typeof element === "boolean") {
visitor.visitBoolean(element);
}
}
}
// 进行访问
visitElements(elements, visitor);
在这个示例中,我们首先定义了一个访问者对象。然后我们定义了一组被访问元素,并将它们放入一个数组中。最后,我们定义了一个 visitElements 函数,该函数接受被访问元素和访问者作为参数,并遍历被访问元素数组,对每个元素调用适当的访问方法。
这里我们只提供了对 Number 和 Boolean 类型元素的访问方法,但实际上,我们可以轻松地添加新的访问方法,以支持其他类型的元素。
通过使用访问者模式,我们可以将访问过程与具体的元素结构分离开来,从而使得访问者可以独立于具体的元素结构变化。在实现中,访问者不需要知道具体元素结构的细节,而元素也不需要知道如何被访问,因此能够提高代码的复用性、灵活性和可维护性。