JavaScript 设计模式

设计模式是为我们编写代码和解决常见问题提供一种框架。

在 JavaScript 中,设计模式可以分为三大类:创建型模式、结构型模式和行为型模式。

下面我们将详细介绍每种模式以及应用场景和代码示例。

创建型模式

创建型设计模式专注于处理对象创建机制,以合适的情况创建对象。
这包括了工厂模式、抽象工厂模式、建造者模式、原型模式、单例模式。

工厂模式

工厂模式(Factory Pattern)是一种创建型设计模式,用于创建对象的过程中封装实例化的逻辑。它提供了一种统一的接口来实例化对象,而不需要直接使用构造函数。

应用场景:

  • 当创建对象的过程比较复杂,包含多个步骤或依赖关系时,可以使用工厂模式来将这些步骤封装起来,使代码更加清晰和可维护。
  • 当需要根据不同的条件创建不同类型的对象时,可以使用工厂模式来根据条件动态地创建相应的对象。
// 定义汽车类
class Car {
  constructor(options) {
    this.doors = options.doors || 4; // 汽车的门数,默认为4
    this.state = options.state || "brand new"; // 汽车的状态,默认为全新
    this.color = options.color || "silver"; // 汽车的颜色,默认为银色
  }
}

// 定义汽车工厂类
class CarFactory {
  // 创建汽车的方法
  createCar(options) {
    return new Car(options); // 使用传入的选项创建新的汽车对象
  }
}

// 创建汽车工厂实例
const factory = new CarFactory();

// 使用汽车工厂创建汽车对象
const myCar = factory.createCar({ color: "blue", state: "used" });

console.log(myCar); // 输出:Car { doors: 4, state: 'used', color: 'blue' }

通过使用汽车工厂类,将创建汽车的逻辑封装起来,以便我们可以轻松地创建具有不同选项的汽车对象。在这个示例中,我们创建了一个蓝色且状态为已使用的汽车对象 myCar,并将其打印到控制台上。

抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,用于提供一个接口来创建一系列相关或相互依赖的对象,而无需指定具体的类。

应用场景:

  • 当需要创建一系列相关的对象,且这些对象之间有一定的约束或依赖关系时,可以使用抽象工厂模式来确保对象的一致性和完整性。
  • 当希望隐藏具体对象的创建逻辑,只通过一个公共接口来创建对象时,可以使用抽象工厂模式来封装实例化的过程。
// 定义汽车类
class Car {
  constructor(options) {
    this.doors = options.doors || 4; // 汽车的门数,默认为4
    this.state = options.state || "brand new"; // 汽车的状态,默认为全新
    this.color = options.color || "silver"; // 汽车的颜色,默认为银色
  }
}

// 定义卡车类
class Truck {
  constructor(options) {
    this.state = options.state || "used"; // 卡车的状态,默认为已使用
    this.wheelSize = options.wheelSize || "large"; // 卡车的轮胎尺寸,默认为大号
  }
}

// 定义抽象车辆工厂类
class AbstractVehicleFactory {
  constructor() {
    this.types = {}; // 存储车辆类型和对应类的映射关系
  }

  // 根据类型和选项获取车辆对象
  getVehicle(type, options) {
    const Vehicle = this.types[type];
    return Vehicle ? new Vehicle(options) : null; // 使用对应的类创建车辆对象
  }

  // 注册车辆类型和对应的类
  registerVehicle(type, Vehicle) {
    this.types[type] = Vehicle;
  }
}

// 创建抽象车辆工厂实例
const factory = new AbstractVehicleFactory();

// 注册车辆类型和对应的类
factory.registerVehicle("car", Car);
factory.registerVehicle("truck", Truck);

// 使用工厂获取车辆对象
const car = factory.getVehicle("car", { color: "yellow" });

console.log(car); // 输出:Car { doors: 4, state: 'brand new', color: 'yellow' }

在这个示例中,我们定义了 Car 汽车类和 Truck 卡车类,并创建了一个抽象车辆工厂类 AbstractVehicleFactory。通过该工厂类,我们可以注册不同类型的车辆,并通过指定类型和选项来获取对应的车辆对象。在示例中,我们通过工厂获取了一个黄色的汽车对象 car,并将其打印到控制台上。

建造者模式

建造者模式(Builder Pattern)是一种创建型设计模式,用于将复杂对象的构建过程和表示分离。它允许按步骤构建对象,同时隐藏构建细节。

应用场景:

  • 当创建对象的构建过程较为复杂,需要按照一定的顺序执行多个步骤来构建对象时,可以使用建造者模式。
  • 当希望创建不同属性组合的对象时,可以使用建造者模式来灵活地构建对象。
// 定义汽车建造者类
class CarBuilder {
  constructor() {
    this.car = new Car(); // 创建一个汽车实例
  }

  // 设置汽车的门数
  setNumDoors(doors) {
    this.car.doors = doors;
    return this; // 返回当前建造者实例,以支持链式调用
  }

  // 设置汽车的颜色
  setColor(color) {
    this.car.color = color;
    return this; // 返回当前建造者实例,以支持链式调用
  }

  // 构建汽车对象
  build() {
    return this.car;
  }
}

// 定义汽车类
class Car {
  constructor() {
    this.doors = 4; // 汽车的门数,默认为4
    this.color = "white"; // 汽车的颜色,默认为白色
  }
}

// 创建汽车建造者实例
const builder = new CarBuilder();

// 使用建造者设置汽车的属性并构建汽车对象
const myCar = builder.setNumDoors(2).setColor("red").build();

console.log(myCar); // 输出:Car { doors: 2, color: 'red' }

在这个示例中,我们定义了 CarBuilder 汽车建造者类和 Car 汽车类。通过使用汽车建造者,我们可以按照需要设置汽车的属性,并最终构建出一个具有特定属性的汽车对象。在示例中,我们设置了汽车的门数为 2,颜色为红色,并通过建造者构建了汽车对象 myCar,然后将其打印到控制台上。

原型模式

原型模式(Prototype Pattern)是一种创建型设计模式,用于通过复制现有对象来创建新对象,而无需显式地使用构造函数。

应用场景:

  • 当创建对象的成本较高,或者对象的创建过程较为复杂时,可以使用原型模式来避免重复执行创建步骤,提高效率。
  • 当需要创建多个相似对象,并希望它们之间相互独立,即修改一个对象不会影响其他对象时,可以使用原型模式。
// 定义汽车原型对象
const carPrototype = {
  init: function (carModel) {
    this.model = carModel;
  },

  getModel: function () {
    return this.model;
  }
};

// 创建汽车函数,用于创建新的汽车对象
function createCar(model) {
  function F() {}
  F.prototype = carPrototype;

  const f = new F();
  f.init(model);
  return f;
}

// 使用创建汽车函数创建一个新的汽车对象
const myCar = createCar("Tesla Model 3");
console.log(myCar.getModel()); // 输出:'Tesla Model 3'

在这个示例中,我们定义了一个汽车原型对象 carPrototype,它包含了初始化和获取模型的方法。然后,我们创建了一个 createCar 函数,用于通过复制原型对象来创建新的汽车对象。通过调用 createCar 函数,并传入特定的车型参数,我们创建了一个新的汽车对象 myCar,并打印出它的模型。

单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,用于确保类只有一个实例,并提供全局访问点。

应用场景:

  • 当需要在系统中共享某个资源,而只能有一个实例访问该资源时,可以使用单例模式。
  • 当需要限制某个类的实例化次数,确保只有一个实例存在时,可以使用单例模式。
// 定义单例类
class Singleton {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }

  // 静态方法,用于获取单例实例
  static getInstance(name) {
    if (!this.instance) {
      this.instance = new Singleton(name);
    }

    return this.instance;
  }
}

// 使用单例类的静态方法获取实例
const singleton1 = Singleton.getInstance("Singleton1");
const singleton2 = Singleton.getInstance("Singleton2");

console.log(singleton1 === singleton2); // 输出:true

在这个示例中,我们定义了一个单例类 Singleton,其中包含一个实例变量 instance,用于存储单例实例。通过静态方法 getInstance,我们可以获取该单例实例。在示例中,我们通过调用 Singleton.getInstance 方法两次,分别传入不同的名称参数,然后将获取到的两个实例进行比较,得到结果为 true,证明这两个实例是同一个实例。

结构型模式

结构型设计模式考虑了对象之间的关系,确保对象能正常的协同工作。这包括了适配器模式、桥接模式、装饰者模式、外观模式、享元模式、组合模式和代理模式。

适配器模式

适配器模式(Adapter Pattern)是一种结构型设计模式,用于将一个类的接口转换成另一个类的接口,使得原本不兼容的类能够合作。

应用场景:

  • 当需要将现有的类或接口与其他类或接口进行协同工作时,可以使用适配器模式。
  • 当需要复用已有的类,但其接口与系统要求不一致时,可以使用适配器模式。
// 老版本API类
class OldAPI {
  oldMethod() {
    return "oldResult";
  }
}

// 新版本API类
class NewAPI {
  newMethod() {
    return "newResult";
  }
}

// 适配器类
class Adapter {
  constructor() {
    this.oldAPI = new OldAPI(); // 创建老版本API的实例
  }

  newMethod() {
    return this.oldAPI.oldMethod(); // 调用老版本API的方法
  }
}

// 创建适配器实例
const adapter = new Adapter();

console.log(adapter.newMethod()); // 输出:'oldResult'

在这个示例中,我们有一个老版本的 API 类 OldAPI 和一个新版本的 API 类 NewAPI。为了让新版本的 API 能够使用旧版本的 API,我们创建了一个适配器类 Adapter。适配器类内部持有旧版本 API 的实例,并提供了新版本 API 的方法。在适配器类的 newMethod 方法中,我们调用了旧版本 API 的方法。通过适配器类,我们可以使用新版本 API 的方式来调用旧版本 API 的功能。在示例中,我们创建了一个适配器实例 adapter,并调用其 newMethod 方法,得到了 ‘oldResult’。

桥接模式

桥接模式(Bridge Pattern)是一种结构型设计模式,用于将抽象部分与实现部分分离,使它们可以独立变化。

应用场景:

  • 当需要通过多个维度或多个层次进行扩展时,可以使用桥接模式来避免类爆炸问题。
  • 当希望抽象部分和实现部分能够独立扩展,且两者之间存在多对多的关系时,可以使用桥接模式。
// 电视类
class TV {
  on() {
    console.log("TV is on");
  }

  off() {
    console.log("TV is off");
  }
}

// 遥控器类
class RemoteControl {
  constructor(device) {
    this.device = device;
  }

  turnOn() {
    this.device.on();
  }

  turnOff() {
    this.device.off();
  }
}

// 创建电视实例
const tv = new TV();
// 创建遥控器实例,并将电视实例作为设备传入
const remote = new RemoteControl(tv);

remote.turnOn(); // 输出:'TV is on'
remote.turnOff(); // 输出:'TV is off'

在这个示例中,我们有一个电视类 TV 和一个遥控器类 RemoteControl。遥控器类接受一个设备(如电视)作为参数。通过遥控器类的 turnOn 和 turnOff 方法,我们可以操作传入的设备,而不需要直接与设备进行交互。在示例中,我们创建了一个电视实例 tv,然后将其传入遥控器实例 remote。通过遥控器实例的方法,我们可以控制电视的开关状态。

装饰者模式

装饰者模式(Decorator Pattern)是一种结构型设计模式,用于动态地给对象添加新的行为或责任,而无需修改其原始代码。

应用场景:

  • 当需要在不改变现有对象结构的情况下,对对象进行功能扩展或行为增加时,可以使用装饰者模式。
  • 当需要在运行时动态地添加或删除对象的功能时,可以使用装饰者模式。
// 汽车类
class Car {
  constructor() {
    this.cost = function () {
      return 20000;
    };
  }
}

// 添加安全气囊装饰函数
function addAirbags(car) {
  const cost = car.cost();
  car.cost = function () {
    return cost + 4000;
  };
}

// 创建汽车实例
const myCar = new Car();
// 使用装饰函数添加安全气囊
addAirbags(myCar);

console.log(myCar.cost()); // 输出:24000

在这个示例中,我们有一个汽车类 Car,其中包含一个 cost 方法,用于返回汽车的基本价格。然后,我们创建了一个装饰函数 addAirbags,它接受一个汽车对象作为参数,并在其基础上添加了安全气囊的价格。通过调用装饰函数,我们可以动态地为汽车对象添加新的功能(安全气囊),而无需修改汽车类的原始代码。最后,我们调用 myCar.cost() 方法来获取装饰后的汽车价格,得到结果为 24000。

外观模式

外观模式(Facade Pattern)是一种结构型设计模式,提供了一个简化的接口,用于访问复杂系统中的一组接口。

应用场景:

  • 当需要简化复杂系统的接口,提供一个更简单、更高级别的接口给客户端使用时,可以使用外观模式。
  • 当希望将子系统与客户端解耦,使得客户端不直接与子系统交互时,可以使用外观模式。
// 外观类
class Facade {
  start() {
    console.log("start");
  }

  stop() {
    console.log("stop");
  }
}

// 汽车类
class Car {
  constructor() {
    this.engine = new Facade();
  }

  start() {
    this.engine.start();
  }

  stop() {
    this.engine.stop();
  }
}

// 创建汽车实例
const myCar = new Car();

myCar.start(); // 输出:'start'
myCar.stop(); // 输出:'stop'

在这个示例中,我们有一个外观类 Facade,它提供了 start 和 stop 两个方法。然后,我们创建了一个汽车类 Car,其中包含一个外观类的实例 engine。通过汽车类的 start 和 stop 方法,我们可以间接地调用外观类的相应方法,从而实现了对复杂系统的简化访问。最后,我们调用 myCar.start() 和 myCar.stop() 方法,分别输出 ‘start’ 和 ‘stop’。

享元模式

享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来最小化内存使用和性能开销,以支持大量细粒度对象的有效管理。

应用场景:

  • 当需要创建大量相似对象,并且这些对象的部分状态可以共享时,可以使用享元模式。
  • 当希望减少内存使用或提高性能,通过共享对象来减少重复创建相似对象时,可以使用享元模式。
// 冰淇淋类
class IceCream {
  constructor(flavor, price) {
    this.flavor = flavor;
    this.price = price;
  }
}

// 冰淇淋工厂类
class IceCreamFactory {
  constructor() {
    this._icecreams = [];
  }

  // 创建或获取冰淇淋对象
  createIceCream(flavor, price) {
    let icecream = this.getIceCream(flavor, price);
    if (icecream) {
      return icecream; // 如果已存在相同属性的冰淇淋对象,则直接返回该对象
    } else {
      const newIceCream = new IceCream(flavor, price); // 否则创建新的冰淇淋对象
      this._icecreams.push(newIceCream); // 将新对象存储起来
      return newIceCream;
    }
  }

  // 获取已存在的冰淇淋对象
  getIceCream(flavor, price) {
    return this._icecreams.find(
      (icecream) => icecream.flavor === flavor && icecream.price === price
    );
  }
}

// 创建冰淇淋工厂实例
const factory = new IceCreamFactory();

// 创建两个相同属性的冰淇淋对象
const choco1 = factory.createIceCream("chocolate", 15);
const choco2 = factory.createIceCream("chocolate", 15);

console.log(choco1 === choco2); // 输出:true,两个对象是同一个对象的引用

在这个示例中,我们有一个冰淇淋类 IceCream 和一个冰淇淋工厂类 IceCreamFactory。工厂类用于创建冰淇淋对象,并在创建之前先检查是否已存在具有相同属性的冰淇淋对象。如果已存在,则直接返回该对象;否则,创建新的冰淇淋对象并存储起来。通过工厂类的 createIceCream 方法,我们可以获取相同属性的冰淇淋对象,并且两个对象是同一个对象的引用。在示例中,我们创建了两个相同属性的冰淇淋对象 choco1 和 choco2,并通过比较它们的引用,得到结果为 true。

组合模式

组合模式(Composite Pattern)是一种结构型设计模式,用于将对象组合成树形结构以表示“部分-整体”的层次结构。它使得用户对单个对象和组合对象的使用具有一致性。

应用场景:

  • 当需要表示对象的层次结构,并且希望用户能够以一致的方式处理单个对象和组合对象时,可以使用组合模式。
  • 当希望将对象的集合组织成树状结构,并且能够以递归的方式处理这些对象时,可以使用组合模式。
// 组件类
class Component {
  constructor(name) {
    this._name = name;
  }

  getNodeName() {
    return this._name;
  }
}

// 叶子节点类
class Leaf extends Component {
  constructor(name) {
    super(name);
  }
}

// 组合节点类
class Composite extends Component {
  constructor(name) {
    super(name);
    this._children = [];
  }

  add(child) {
    this._children.push(child);
    return this;
  }

  getNodeName() {
    return (
      this._name +
      " " +
      this._children.map((child) => child.getNodeName()).join(" ")
    );
  }
}

// 创建组合结构
const tree = new Composite("tree");
tree.add(new Leaf("leaf1"));
tree.add(new Leaf("leaf2"));

console.log(tree.getNodeName()); // 输出:'tree leaf1 leaf2'

在这个示例中,我们有一个组件类 Component,它是组合模式的基类。我们派生出两个子类:叶子节点类 Leaf 和组合节点类 Composite。叶子节点表示树结构的叶子节点,而组合节点表示树结构的分支节点。组合节点可以包含其他组合节点或叶子节点作为子节点。

我们创建一个名为 tree 的组合节点,并向其添加两个叶子节点 leaf1 和 leaf2。通过调用 tree.getNodeName() 方法,我们可以获取整个组合结构的节点名称。在示例中,输出结果为 ‘tree leaf1 leaf2’,表示整个组合结构的节点名称。

代理模式

代理模式(Proxy Pattern)是一种结构型设计模式,用于提供一个代理对象来控制对原始对象的访问。代理对象充当了客户端与原始对象之间的中介,从而可以在访问对象时添加额外的功能。

应用场景:

  • 当需要控制对某个对象的访问,并且希望在访问时添加额外的逻辑或限制时,可以使用代理模式。
  • 当需要延迟创建对象,以提高系统性能或减少资源消耗时,可以使用代理模式。
  • 当需要在访问对象时执行额外操作,例如对象缓存、权限验证等,可以使用代理模式。
// 真实图像类
class RealImage {
  constructor(filename) {
    this._filename = filename;
    this.loadFromDisk();
  }

  loadFromDisk() {
    console.log(`Loading... ${this._filename}`);
  }

  display() {
    console.log(`Displaying... ${this._filename}`);
  }
}

// 代理图像类
class ProxyImage {
  constructor(filename) {
    this._filename = filename;
    this._image = null;
  }

  display() {
    if (this._image === null) {
      this._image = new RealImage(this._filename); // 延迟创建真实图像对象
    }
    this._image.display(); // 调用真实图像对象的 display 方法
  }
}

// 创建代理图像对象
const image = new ProxyImage("test.jpg");

image.display(); // 输出:'Loading... test.jpg' 'Displaying... test.jpg'

在这个示例中,我们有一个真实图像类 RealImage 和一个代理图像类 ProxyImage。代理图像类控制对真实图像的访问,并在需要时延迟创建真实图像对象。当调用代理图像对象的 display 方法时,如果真实图像对象尚未创建,代理图像对象将创建真实图像对象,并调用其 display 方法。如果真实图像对象已创建,代理图像对象将直接调用其 display 方法。

在示例中,我们创建了一个代理图像对象 image,并调用其 display 方法。由于真实图像对象尚未创建,代理图像对象会打印出 ‘Loading… test.jpg’,然后创建真实图像对象,并调用其 display 方法,打印出 ‘Displaying… test.jpg’。

行为型模式

行为型设计模式关注对象之间的责任分配。这包括了责任链模式、命令模式、迭代器模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式、中介者模式和备忘录模式。

责任链模式

责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,用于将请求的发送者和接收者解耦,并通过一条链路传递请求,直到有一个处理者能够处理该请求为止。

应用场景:

  • 当有多个对象可以处理同一请求,并且在运行时才确定哪个对象来处理时,可以使用责任链模式。
  • 当希望避免请求发送者和接收者之间的耦合关系,以及避免请求在系统内部传递时的显式引用时,可以使用责任链模式。
// 处理者基类
class Handler {
  setSuccessor(successor) {
    this.successor = successor;
  }

  handleRequest() {
    throw new Error("Abstract method!");
  }
}

// 具体处理者1
class ConcreteHandler1 extends Handler {
  handleRequest(request) {
    if (request === "run") {
      return "I can run";
    } else if (this.successor) {
      return this.successor.handleRequest(request);
    }
  }
}

// 具体处理者2
class ConcreteHandler2 extends Handler {
  handleRequest(request) {
    if (request === "jump") {
      return "I can jump";
    } else if (this.successor) {
      return this.successor.handleRequest(request);
    }
  }
}

// 创建具体处理者实例
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();

// 设置处理者之间的关系
handler1.setSuccessor(handler2);

console.log(handler1.handleRequest("jump")); // 输出:'I can jump'

在这个示例中,我们有一个处理者基类 Handler,其中包含了设置后继处理者的方法 setSuccessor 和抽象的处理请求方法 handleRequest。我们派生出两个具体处理者类 ConcreteHandler1 和 ConcreteHandler2,它们分别实现了处理请求的方法。如果当前处理者能够处理请求,就返回处理结果;否则,将请求传递给后继处理者。

我们创建了两个具体处理者实例 handler1 和 handler2,并通过调用 setSuccessor 方法将它们关联起来,形成一个处理者链。然后,我们调用 handler1.handleRequest(‘jump’) 方法来处理请求,根据处理者链的设计,会找到具体处理者 2 来处理该请求,最终输出 ‘I can jump’。

命令模式

命令模式(Command Pattern)是一种行为型设计模式,用于将请求封装成对象,从而使得可以用不同的请求对客户端进行参数化。

应用场景:

  • 当需要将请求发送者和请求接收者解耦,并且希望以一种可扩展的方式来组织和执行请求时,可以使用命令模式。
  • 当需要支持撤销、重做等操作,或者需要实现任务队列、日志记录等功能时,可以使用命令模式。
// 接收者类
class Receiver {
  action() {
    console.log("action has been taken.");
  }
}

// 命令类
class Command {
  constructor(receiver) {
    this.receiver = receiver;
  }

  execute() {
    this.receiver.action();
  }
}

// 调用者类
class Invoker {
  setCommand(cmd) {
    this.cmd = cmd;
  }

  executeCommand() {
    this.cmd.execute();
  }
}

// 创建接收者实例
const receiver = new Receiver();
// 创建命令实例,并将接收者传入
const cmd = new Command(receiver);
// 创建调用者实例
const invoker = new Invoker();

// 设置调用者的命令
invoker.setCommand(cmd);
// 执行命令
invoker.executeCommand(); // 输出:'action has been taken.'

在这个示例中,我们有一个接收者类 Receiver,其中包含了一个 action 方法,表示接收者执行的动作。我们还有一个命令类 Command,它接受一个接收者作为参数,在 execute 方法中调用接收者的动作方法。最后,我们有一个调用者类 Invoker,它可以设置命令,并通过调用 executeCommand 方法来执行命令。

我们创建了一个接收者实例 receiver,一个命令实例 cmd(将接收者传入),以及一个调用者实例 invoker。通过设置调用者的命令为 cmd,然后执行命令,最终会调用接收者的动作方法,输出 ‘action has been taken.’。

迭代器模式

迭代器模式(Iterator Pattern)是一种行为型设计模式,用于提供一种方法来顺序访问聚合对象中的元素,而无需暴露其内部表示。

应用场景:

  • 当需要以一种统一的方式遍历不同类型的聚合对象,而又不希望暴露其内部结构时,可以使用迭代器模式。
  • 当需要在遍历过程中对聚合对象进行增删操作,而又不希望影响遍历逻辑时,可以使用迭代器模式。
// 迭代器类
class Iterator {
  constructor(items) {
    this.index = 0;
    this.items = items;
  }

  next() {
    return this.items[this.index++];
  }

  hasNext() {
    return this.index < this.items.length;
  }
}

// 创建迭代器实例,并传入要遍历的数组
const iterator = new Iterator(["apple", "banana", "cherry"]);

// 使用迭代器遍历数组并输出元素
while (iterator.hasNext()) {
  console.log(iterator.next());
}

在这个示例中,我们有一个迭代器类 Iterator,它接收一个数组作为参数,并提供了 next 和 hasNext 方法来顺序访问数组中的元素。在迭代器类中,我们使用 index 来记录当前访问的元素位置,并在 next 方法中返回该位置的元素,并将 index 自增。在 hasNext 方法中,我们检查 index 是否小于数组长度,以确定是否还有下一个元素可访问。

然后,我们创建了一个迭代器实例 iterator,并传入要遍历的数组 [‘apple’, ‘banana’, ‘cherry’]。通过使用 while 循环和迭代器的 hasNext 和 next 方法,我们可以遍历数组并逐个输出元素。

观察者模式

观察者模式(Observer Pattern)是一种行为型设计模式,用于在对象之间定义一对多的依赖关系,使得当一个对象的状态发生变化时,所有依赖它的对象都能得到通知并自动更新。

应用场景:

  • 当一个对象的改变需要同时影响其他对象,并且不希望这些对象之间紧密耦合时,可以使用观察者模式。
  • 当需要在对象之间建立一种一对多的依赖关系,并且希望对象之间的依赖关系是松散耦合的时候,可以使用观察者模式。
// 主题类
class Subject {
  constructor() {
    this.observers = [];
  }

  // 订阅观察者
  subscribe(observer) {
    this.observers.push(observer);
  }

  // 取消订阅观察者
  unsubscribe(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  // 通知观察者更新
  notify(data) {
    this.observers.forEach((observer) => observer.update(data));
  }
}

// 观察者类
class Observer {
  // 抽象的更新方法
  update() {
    throw new Error("This is an abstract method.");
  }
}

// 具体观察者类
class ConcreteObserver extends Observer {
  // 实现具体的更新方法
  update(data) {
    console.log(`Observer received data: ${data}`);
  }
}

// 创建主题实例
const subject = new Subject();
// 创建观察者实例
const observer = new ConcreteObserver();

// 订阅观察者
subject.subscribe(observer);
// 通知观察者更新
subject.notify("Hello, world!"); // 输出:'Observer received data: Hello, world!'

在这个示例中,我们有一个主题类 Subject,其中维护了一个观察者列表 observers。主题类提供了订阅观察者、取消订阅观察者以及通知观察者更新的方法。在通知方法中,我们遍历观察者列表,调用每个观察者的更新方法。

我们还有一个抽象观察者类 Observer,其中定义了一个抽象的更新方法。然后,我们派生出一个具体观察者类 ConcreteObserver,它实现了具体的更新方法,用于接收并处理主题发来的数据。

我们创建了一个主题实例 subject 和一个观察者实例 observer。通过调用主题实例的订阅方法将观察者订阅到主题中,然后调用主题实例的通知方法来通知观察者更新。在示例中,观察者收到了数据 ‘Hello, world!’ 并打印出 ‘Observer received data: Hello, world!’。

状态模式

状态模式(State Pattern)是一种行为型设计模式,用于在对象内部封装不同的状态,并允许对象在状态发生变化时改变其行为。

应用场景:

  • 当一个对象的行为取决于其内部状态,并且需要在运行时根据状态来改变行为时,可以使用状态模式。
  • 当需要将复杂的条件分支语句转化为对象的方法调用,并且希望状态的变化能够自动更新对象行为时,可以使用状态模式。
// 状态抽象类
class State {
  handle(context) {
    throw new Error("This is an abstract method.");
  }
}

// 具体状态类 A
class ConcreteStateA extends State {
  handle(context) {
    console.log("ConcreteStateA handle");
    context.state = new ConcreteStateB();
  }
}

// 具体状态类 B
class ConcreteStateB extends State {
  handle(context) {
    console.log("ConcreteStateB handle");
    context.state = new ConcreteStateA();
  }
}

// 上下文类
class Context {
  constructor() {
    this.state = new ConcreteStateA();
  }

  // 请求方法
  request() {
    this.state.handle(this);
  }
}

// 创建上下文实例
const context = new Context();
context.request(); // 输出:'ConcreteStateA handle'
context.request(); // 输出:'ConcreteStateB handle'

在这个示例中,我们有一个状态抽象类 State,其中定义了一个抽象的处理方法 handle。然后,我们派生出两个具体状态类 ConcreteStateA 和 ConcreteStateB,它们分别实现了处理方法,并根据需要更新上下文的状态。

我们还有一个上下文类 Context,它内部维护了一个当前状态对象。上下文类提供了一个请求方法 request,用于触发状态对象的处理方法。在处理方法中,我们根据当前状态执行相应的行为,并根据需要更新上下文的状态。

我们创建了一个上下文实例 context,并连续两次调用请求方法 request。第一次调用时,当前状态是 ConcreteStateA,执行了 ConcreteStateA 的处理方法,并将上下文的状态更新为 ConcreteStateB。第二次调用时,当前状态是 ConcreteStateB,执行了 ConcreteStateB 的处理方法,并将上下文的状态更新为 ConcreteStateA。

策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法封装到独立的策略类中,并使它们可以互相替换。策略模式使得算法的变化独立于使用算法的客户端。

应用场景:

  • 当需要在不同情况下使用不同的算法,并希望能够在运行时动态地选择和切换算法时,可以使用策略模式。
  • 当需要将算法的实现细节与使用算法的代码分离,以避免代码的复杂性和臃肿性时,可以使用策略模式。
// 上下文类
class Context {
  constructor(strategy) {
    this.strategy = strategy;
  }

  // 上下文接口方法
  contextInterface() {
    this.strategy.algorithmInterface();
  }
}

// 策略抽象类
class Strategy {
  // 策略算法接口方法(抽象方法)
  algorithmInterface() {
    throw new Error("This is an abstract method.");
  }
}

// 具体策略类 A
class ConcreteStrategyA extends Strategy {
  // 实现具体策略算法方法
  algorithmInterface() {
    console.log("ConcreteStrategyA algorithmInterface");
  }
}

// 具体策略类 B
class ConcreteStrategyB extends Strategy {
  // 实现具体策略算法方法
  algorithmInterface() {
    console.log("ConcreteStrategyB algorithmInterface");
  }
}

// 创建具体策略实例
const strategyA = new ConcreteStrategyA();
const strategyB = new ConcreteStrategyB();

// 创建上下文实例,并传入具体策略实例
let context = new Context(strategyA);
context.contextInterface(); // 输出:'ConcreteStrategyA algorithmInterface'

// 切换具体策略实例并再次调用上下文接口方法
context = new Context(strategyB);
context.contextInterface(); // 输出:'ConcreteStrategyB algorithmInterface'

在这个示例中,我们有一个上下文类 Context,其中接受一个策略对象作为构造函数的参数,并在上下文接口方法中调用策略对象的算法接口方法。

我们还有一个策略抽象类 Strategy,其中定义了一个抽象的算法接口方法。然后,我们派生出两个具体策略类 ConcreteStrategyA 和 ConcreteStrategyB,它们分别实现了算法接口方法的具体算法。

我们创建了具体策略实例 strategyA 和 strategyB。然后,我们创建了一个上下文实例 context,并传入具体策略实例 strategyA,调用上下文接口方法,输出 ‘ConcreteStrategyA algorithmInterface’。接着,我们将具体策略实例切换为 strategyB,再次调用上下文接口方法,输出 ‘ConcreteStrategyB algorithmInterface’。

通过策略模式,我们可以在运行时动态地选择和切换不同的策略对象,从而实现不同的算法行为。这种灵活性和可扩展性使得策略模式在许多场景下非常有用。

模板方法模式

模板方法模式(Template Method Pattern)是一种行为型设计模式,它定义了一个算法的骨架,并允许子类在不改变算法结构的情况下重写算法的特定步骤。

应用场景:

  • 当有一个算法的整体结构已经确定,但其中某些步骤的具体实现可能有所不同,可以使用模板方法模式。
  • 当需要在不改变算法整体结构的情况下,通过子类来定制化算法的某些步骤时,可以使用模板方法模式。
// 抽象类
class AbstractClass {
  // 抽象方法,子类需要实现具体的操作
  primitiveOperation1() {
    throw new Error("This is an abstract method.");
  }

  // 抽象方法,子类需要实现具体的操作
  primitiveOperation2() {
    throw new Error("This is an abstract method.");
  }

  // 模板方法,定义算法的骨架
  templateMethod() {
    this.primitiveOperation1();
    this.primitiveOperation2();
    console.log("Template method is called.");
  }
}

// 具体类 A
class ConcreteClassA extends AbstractClass {
  // 实现具体的操作1
  primitiveOperation1() {
    console.log("ConcreteClassA primitiveOperation1");
  }

  // 实现具体的操作2
  primitiveOperation2() {
    console.log("ConcreteClassA primitiveOperation2");
  }
}

// 具体类 B
class ConcreteClassB extends AbstractClass {
  // 实现具体的操作1
  primitiveOperation1() {
    console.log("ConcreteClassB primitiveOperation1");
  }

  // 实现具体的操作2
  primitiveOperation2() {
    console.log("ConcreteClassB primitiveOperation2");
  }
}

let concrete = new ConcreteClassA();
concrete.templateMethod();
/* 输出:
'ConcreteClassA primitiveOperation1'
'ConcreteClassA primitiveOperation2'
'Template method is called.'
*/

concrete = new ConcreteClassB();
concrete.templateMethod();
/* 输出:
'ConcreteClassB primitiveOperation1'
'ConcreteClassB primitiveOperation2'
'Template method is called.'
*/

在这个示例中,我们有一个抽象类 AbstractClass,其中定义了模板方法 templateMethod,它包含了算法的骨架。在模板方法中,我们调用了两个抽象方法 primitiveOperation1 和 primitiveOperation2,它们由子类来实现具体的操作。

我们派生出两个具体类 ConcreteClassA 和 ConcreteClassB,它们分别实现了抽象方法中的具体操作。每个具体类都重写了父类的抽象方法,以定义自己的特定行为。

然后,我们创建了一个具体类 concrete,首先将其实例化为 ConcreteClassA,并调用模板方法 templateMethod,输出了具体类 ConcreteClassA 中实现的操作。接着,我们将具体类切换为 ConcreteClassB,再次调用模板方法,输出了具体类 ConcreteClassB 中实现的操作。

通过模板方法模式,我们可以将算法的骨架固定下来,并允许子类按需重写特定步骤,从而实现定制化的算法行为。

访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,用于在不改变被访问对象的类的前提下,定义作用于这些对象上的新操作。

应用场景:

  • 当需要对一个复杂对象结构进行操作,并且希望将操作与对象的结构分离时,可以使用访问者模式。
  • 当对一个对象结构中的各个元素进行不同的处理,而又不希望在每个元素的类中添加该处理逻辑时,可以使用访问者模式。
// 访问者类
class Visitor {
  // 访问具体元素 A 的方法
  visitConcreteElementA(concreteElementA) {
    console.log(
      `${concreteElementA.constructor.name} visited by ${this.constructor.name}`
    );
  }

  // 访问具体元素 B 的方法
  visitConcreteElementB(concreteElementB) {
    console.log(
      `${concreteElementB.constructor.name} visited by ${this.constructor.name}`
    );
  }
}

// 具体元素 A
class ConcreteElementA {
  // 接受访问者的方法
  accept(visitor) {
    visitor.visitConcreteElementA(this);
  }
}

// 具体元素 B
class ConcreteElementB {
  // 接受访问者的方法
  accept(visitor) {
    visitor.visitConcreteElementB(this);
  }
}

// 对象结构类
class ObjectStructure {
  constructor() {
    this.elements = [];
  }

  // 添加元素
  attach(element) {
    this.elements.push(element);
  }

  // 移除元素
  detach(element) {
    const index = this.elements.indexOf(element);
    if (index > -1) {
      this.elements.splice(index, 1);
    }
  }

  // 接受访问者的方法
  accept(visitor) {
    this.elements.forEach((element) => {
      element.accept(visitor);
    });
  }
}

// 创建对象结构实例
const o = new ObjectStructure();
o.attach(new ConcreteElementA());
o.attach(new ConcreteElementB());

// 创建访问者实例
const v = new Visitor();

// 对象结构接受访问者访问
o.accept(v);

在这个示例中,我们有一个访问者类 Visitor,其中定义了访问具体元素 A 和具体元素 B 的方法。这些方法用于访问并操作不同类型的元素。

我们还有具体元素类 ConcreteElementA 和 ConcreteElementB,它们分别实现了接受访问者的方法 accept。在这个方法中,我们将具体元素对象自身作为参数传递给访问者对象,以便访问者可以执行相应的操作。

另外,我们有一个对象结构类 ObjectStructure,它用于管理元素的集合。我们可以通过 attach 方法添加元素,通过 detach 方法移除元素,然后通过 accept 方法接受访问者的访问。

在示例中,我们创建了对象结构实例 o,并向其添加了具体元素 A 和具体元素 B。然后,我们创建了访问者实例 v。

最后,我们调用对象结构的 accept 方法,将访问者对象传递给对象结构。对象结构会遍历其中的元素,并依次调用每个元素的 accept 方法,让访问者对元素进行访问和操作。

通过访问者模式,我们可以将对象的操作与对象的结构分离,使得操作逻辑可以独立于对象类定义和修改。这种分离可以提高可维护性和可扩展性。

中介者模式

中介者模式(Mediator Pattern)是一种行为型设计模式,它定义了一个中介者对象,用于封装一组对象之间的交互方式。通过中介者对象,各个对象之间不直接进行通信,而是通过中介者来进行消息的传递和协调。

应用场景:

  • 当一组对象之间的通信方式复杂而且相互依赖时,可以使用中介者模式来解耦对象之间的直接交互。
  • 当一个对象需要与很多其他对象进行通信,并且不希望依赖于这些对象的具体类时,可以使用中介者模式。
// 中介者类
class Mediator {
  // 抽象方法,用于接收来自其他对象的通知
  notify(sender, event) {
    throw new Error("This is an abstract method.");
  }
}

// 具体中介者类
class ConcreteMediator extends Mediator {
  constructor(c1, c2) {
    super();
    this.concreteColleague1 = c1;
    this.concreteColleague1.setMediator(this);
    this.concreteColleague2 = c2;
    this.concreteColleague2.setMediator(this);
  }

  // 实现接收通知的方法,根据通知内容做出相应的反应
  notify(sender, event) {
    if (event === "A") {
      console.log("Mediator reacts on A and triggers following operations:");
      this.concreteColleague2.doC();
    }

    if (event === "D") {
      console.log("Mediator reacts on D and triggers following operations:");
      this.concreteColleague1.doA();
      this.concreteColleague2.doD();
    }
  }
}

// 同事类
class Colleague {
  constructor() {
    this.mediator = null;
  }

  // 设置中介者对象
  setMediator(mediator) {
    this.mediator = mediator;
  }
}

// 具体同事类 1
class ConcreteColleague1 extends Colleague {
  // 同事类 1 执行操作 A
  doA() {
    console.log("Colleague1 does A.");
    this.mediator.notify(this, "A");
  }

  // 同事类 1 执行操作 B
  doB() {
    console.log("Colleague1 does B.");
    this.mediator.notify(this, "B");
  }
}

// 具体同事类 2
class ConcreteColleague2 extends Colleague {
  // 同事类 2 执行操作 C
  doC() {
    console.log("Colleague2 does C.");
    this.mediator.notify(this, "C");
  }

  // 同事类 2 执行操作 D
  doD() {
    console.log("Colleague2 does D.");
    this.mediator.notify(this, "D");
  }
}

// 创建具体同事类实例
const c1 = new ConcreteColleague1();
const c2 = new ConcreteColleague2();

// 创建具体中介者类实例,并传入具体同事类实例
const mediator = new ConcreteMediator(c1, c2);

console.log("Client triggers operation A.");
c1.doA(); // 输出:'Colleague1 does A.' 'Mediator reacts on A and triggers following operations:' 'Colleague2 does C.'

console.log("Client triggers operation D.");
c2.doD(); // 输出:'Colleague2 does D.' 'Mediator reacts on D and triggers following operations:' 'Colleague1 does A.' 'Colleague2 does D.'

在示例中,我们有一个中介者类 Mediator,其中定义了一个抽象方法 notify,用于接收来自其他对象的通知。

我们还有具体中介者类 ConcreteMediator,它维护了两个具体同事类 ConcreteColleague1ConcreteColleague2 的实例,并在构造函数中将中介者对象传递给它们。

具体同事类 ConcreteColleague1ConcreteColleague2 都继承自同事类 Colleague,并实现了自己的操作方法。在操作方法中,它们通过中介者对象的 notify 方法发送通知。

我们创建了具体同事类的实例 c1c2,然后创建了具体中介者类的实例 mediator,并将同事类实例传递给中介者对象。

最后,我们通过具体同事类的实例调用操作方法,模拟客户端触发操作。当执行 c1.doA() 时,输出显示同事类 1 执行操作 A,然后中介者对象对操作 A 做出反应,并触发了同事类 2 的操作 C。

当执行 c2.doD() 时,输出显示同事类 2 执行操作 D,然后中介者对象对操作 D 做出反应,并触发了同事类 1 的操作 A,以及同事类 2 的操作 D。

通过中介者模式,我们可以减少对象之间的直接耦合,并将复杂的交互逻辑集中在中介者对象中。这样可以提高代码的可维护性和扩展性。

备忘录模式

备忘录模式(Memento Pattern)是一种行为型设计模式,用于捕获和恢复对象的内部状态,而不破坏其封装性。

应用场景:

  • 当需要保存和恢复对象的状态,以便在将来的某个时间点重新恢复到先前的状态时,可以使用备忘录模式。
  • 当直接访问对象的状态会暴露对象的实现细节,并且希望通过一个中介来保存和恢复状态时,可以使用备忘录模式。
// 发起人类
class Originator {
  constructor(state) {
    this.state = state;
  }

  // 设置备忘录对象
  setMemento(memento) {
    this.state = memento.state;
  }

  // 创建备忘录对象
  createMemento() {
    return new Memento(this.state);
  }
}

// 备忘录类
class Memento {
  constructor(state) {
    this.state = state;
  }
}

// 管理者类
class Caretaker {
  constructor() {
    this.mementos = {};
  }

  // 添加备忘录对象
  addMemento(key, memento) {
    this.mementos[key] = memento;
  }

  // 获取备忘录对象
  getMemento(key) {
    return this.mementos[key];
  }
}

// 创建管理者实例
const caretaker = new Caretaker();

// 创建发起人实例,并设置初始状态
const originator = new Originator("initial state");

// 创建备忘录对象并添加到管理者
caretaker.addMemento("backup1", originator.createMemento());

// 修改发起人的状态
originator.state = "new state";
console.log(originator.state); // 输出 'new state'

// 从管理者获取备忘录对象并恢复状态
originator.setMemento(caretaker.getMemento("backup1"));
console.log(originator.state); // 输出 'initial state'

在示例中,我们有一个发起人类 Originator,其中包含了一个状态属性 state。发起人类具有创建备忘录和设置备忘录的方法,用于创建备忘录对象并保存和恢复状态。

我们还有一个备忘录类 Memento,它用于存储发起人对象的状态。

管理者类 Caretaker 用于管理备忘录对象。它包含了一个备忘录对象的集合,并提供添加和获取备忘录对象的方法。

我们创建了管理者实例 caretaker 和发起人实例 originator,并通过调用 originator.createMemento() 创建了一个备忘录对象,并将其添加到管理者中。

然后,我们修改了发起人的状态,并输出了修改后的状态。接着,通过 originator.setMemento(caretaker.getMemento(‘backup1’)) 方法从管理者中获取备忘录对象,并恢复发起人的状态。

通过备忘录模式,我们可以在不破坏对象封装性的前提下,保存和恢复对象的状态。这样可以实现撤销和重做操作,或者在某些情况下还原到先前的状态。

以上代码示例分别展示了每种设计模式的使用。实际应用时,需要根据具体需求选择合适的设计模式。

总结

设计模式是解决软件设计问题的经验总结和最佳实践的一种形式。

它们提供了在特定情境下有效工作的可重用设计方案。通过使用设计模式,我们可以提高代码的可维护性、灵活性和可扩展性。

在本篇文章中,我们介绍了以下常见的设计模式:

  • 工厂模式(Factory Pattern):通过一个共同的接口创建对象,根据需求返回相应的具体实例。

  • 抽象工厂模式(Abstract Factory Pattern):提供一个创建相关对象的接口,而无需指定具体类。

  • 建造者模式(Builder Pattern):通过一个步骤化的构建过程来创建复杂对象,使得同样的构建过程可以创建不同的表示。

  • 原型模式(Prototype Pattern):通过克隆现有对象来创建新对象,避免了重复创建对象的成本。

  • 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供全局访问点。

  • 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的接口。

  • 桥接模式(Bridge Pattern):将抽象部分与实现部分分离,使它们可以独立地变化。

  • 装饰者模式(Decorator Pattern):动态地为对象添加额外的行为,而无需修改其原始类。

  • 外观模式(Facade Pattern):提供了一个统一的接口,用于访问子系统中的一组接口。

  • 享元模式(Flyweight Pattern):共享对象,以便有效地支持大量细粒度的对象。

  • 组合模式(Composite Pattern):将对象组合成树形结构,以表示部分-整体的层次结构。

  • 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对该对象的访问。

  • 责任链模式(Chain of Responsibility Pattern):将请求的发送者和接收者解耦,通过一条链来处理请求。

  • 命令模式(Command Pattern):将请求封装成对象,从而允许将客户端参数化其他对象。

  • 迭代器模式(Iterator Pattern):提供一种顺序访问聚合对象中各个元素的方法,而无需暴露其内部表示。

  • 观察者模式(Observer Pattern):定义了对象之间的一对多依赖关系,使得当一个对象状态发生变化时,其相关依赖对象都会收到通知并自动更新。

  • 状态模式(State Pattern):允许对象在内部状态改变时改变其行为,从而使其看起来像是改变了其类。

  • 策略模式(Strategy Pattern):定义了一系列算法,将每个算法都封装起来,并使它们可以互相替换。

  • 模板方法模式(Template Method Pattern):定义了一个算法的骨架,将某些步骤的具体实现延迟到子类中。

  • 访问者模式(Visitor Pattern):在不改变对象结构的前提下,定义了对对象结构中各个元素的操作。

  • 中介者模式(Mediator Pattern):通过一个中介者对象来封装一组对象之间的交互方式。

  • 备忘录模式(Memento Pattern):在不破坏对象封装性的前提下,保存和恢复对象的内部状态。

每个设计模式都有自己的特定应用场景和优势。它们可以帮助我们解决不同类型的问题,提供可维护、可扩展和灵活的设计方案。

然而,设计模式并非银弹,不是适用于所有情况的解决方案。在使用设计模式时,我们需要根据具体的问题和需求,权衡其优势和复杂性,选择合适的模式。

最重要的是,设计模式是为了帮助我们编写更好、更可维护的代码,但并不意味着我们应该过度使用设计模式。合适的设计模式应该在代码可读性、可维护性和扩展性之间取得平衡。

本文链接地址: JavaScript 设计模式