JS设计模式之命令模式的用法详解

作者:代码拾荒时间:2026-10-30 23:23:47浏览:421

本文详解JavaScript中的命令模式,阐述其如何解耦请求调用者与执行者。通过封装命令对象,实现命令队列、异步执行及撤销重做功能。对比策略模式,分析其在按钮交互、网络请求批处理及浏览器原生API中的应用场景与实现细节。

相关定义:

  • 应用命令模式的核心目的在于将【请求的发起方】与【具体执行方】进行解耦。

  • 调用者通过持有一个命令对象来间接触发接收者的方法,从而避免直接引用接收者或依赖其【具体实现】细节。

  • 这种解耦机制赋予了系统更高的灵活性,使得【扩展】和修改命令的调用逻辑变得更加容易。

  • 例如,我们可以【将命令对象保存在【队列中】】,从而支持命令的【排队】机制以及【异步执行】场景。

  • 此外,通过记录命令的【历史】,系统能够支持【撤销和重做】等高级操作。

  • 该模式的典型应用场景包括【菜单操作】、【多级撤销】以及【批处理任务】等。

自我理解:

  • 执行者对象E是一个普通对象,它提供了流程中所需的执行方法。该方法对具体命令是可见的,但对调用者而言是不可见的。

  • 抽象命令类A定义了具体命令的基本结构,并为调用者提供统一的调用接口。

  • 具体命令类S实现了抽象命令类,其核心在于封装执行者。准确地说,命令仅封装了执行者的部分方法,抽象方法的执行本质上就是调用这些封装的方法。

  • 调用者对象C封装了两个方法和一个属性;属性用于表示当前命令,第一个方法用于设置或切换具体的命令值,第二个方法用于执行该命令。

  • 由于S封装了E,C只能接触到S,因此C和E实现了完全解耦。

  • A和S之间是一对多的关系,即一个抽象命令可以有多个具体实现。

解耦体现:

  • C无需了解S的具体内容,只需负责触发操作;

  • S无需知晓E如何完成任务,只需调用E暴露出的方法即可;

  • S和E之间是多对多的关系,即一个S可以由多个E的部分方法组合完成,一个E也可以被不同的S封装其不同的部分方法;

  • C维护的不只是一个S,还可以是一个S队列,当触发条件到来时,S队列依次执行。

代码举例1:

// 接收者对象
class Light {
    turnOn() {}
    turnOff() {}
}
// 命令接口
abstract class Command {
    abstract execute():void;
}
// 具体命令:打开灯
class TurnOnCommand extends Command {
    constructor(public light: Light) { super() }
    execute() { this.light.turnOn() }
}
// 具体命令:关闭灯
class TurnOffCommand extends Command {
    constructor(public light: Light) { super() }
    execute() { this.light.turnOff() }
}
// 调用者对象
class RemoteControl {
    command: Command;
    setCommand(command: Command) { this.command = command }
    pressButton() { this.command.execute() }
}
// 使用示例
// 创建执行者
const light = new Light();
// 创建具体命令对象,封装执行者
const turnOnCommand = new TurnOnCommand(light);
const turnOffCommand = new TurnOffCommand(light);
// 创建调用者对象
const remoteControl = new RemoteControl();
// 设置具体命令对象然后执行
remoteControl.setCommand(turnOnCommand);
remoteControl.pressButton();

代码举例2:

使用命令设计模式可以灵活组合网络请求,如下所示:

// 请求接收者
class Reciever {
  async get(path: string) {
    const res = await fetch(`https://rec.example.com/${path}`);
    const data = await res.json();
  }
}
// 命令接口
class Cmd { exe() {} }
// 具体命令:发送请求
class RCd extends Cmd {
  constructor(public rec, public url) {
    super();
  }
  exe() {
    return this.rec.get(this.url);
  }
}
// 调用者对象
class RM {
  cQueue: Cmd[] = [];
  add(command: Cmd) {
    this.cQueue.push(command);
  }
  pReq() {
    const promises = this.cQueue.map((command) => command.exe());
    return Promise.all(promises);
  }
}
// 使用示例
const rec = new Reciever(); // 创建请求接收者
const rM = new RM(); // 创建请求管理者
// 添加具体请求命令
rM.add(new RCd(rec, 'data1'));
rM.add(new RCd(rec, 'data2'));
rM.add(new RCd(rec, 'data3'));
rM.pReq()
  .then(() => {
    console.log('所有请求已完成');
  })
  .catch((error) => {
    console.error('请求出错:', error);
  });

代码举例3:

使用命令设计模式实现撤销和重做,用到了栈数据结构,如下所示:

// 命令接口:想要用命令策略实现撤销、重做就必须先在抽象接口中定义好撤销的接口
abstract class Cmd {
  abstract exe(): void;
  abstract undo(): void;
}
// 具体命令类 - 加法命令
class AddCmd extends Cmd {
  constructor(public rec: Rec, public value: number) {
    super();
  }
  exe() {
    this.rec.add(this.value);
  }
  undo() {
    this.rec.subtract(this.value);
  }
}
// 接收者类
class Rec {
  result = 0;
  add(value: number) { this.result += value }
  subtract(value: number) { this.result -= value }
}
// 调用者/发送者
class Invoker {
  cmds: Cmd[] = [];
  xcmd: Cmd[] = [];
  exe(cmd: Cmd) {
    cmd.exe();
    this.cmds.push(cmd);
  }
  // 重点
  undo() {
    const cmd = this.cmds.pop();
    if (!cmd) return;
    cmd.undo();
    this.xcmd.push(cmd);
  }
  // 重点
  redo() {
    const cmd = this.xcmd.pop();
    if (cmd) {
      cmd.exe();
      this.cmds.push(cmd);
    }
  }
}
// 示例用法
const rec = new Rec(); // 创建接收者对象
const ivk = new Invoker(); // 创建调用者对象
const addCmd = new AddCmd(rec, 5); // 创建加法命令
ivk.exe(addCmd); // 执行加法命令,结果为:5
ivk.undo(); // rec.result = 0
ivk.redo(); // rec.result = 5

命令设计模式和策略设计模式的不同:

  • 命令设计模式的最小操作单元是【命令对象】;而策略设计模式的最小操作单元是方法,或者算法。

  • 命令设计模式一次只操作一个命令对象;而策略设计模式为了完成任务可以组合多个策略。

  • 命令设计模式一般不会将某个命令单独保存到内部状态中;而策略设计模式必须保存当前的策略。

  • 使用命令设计模式可以实现撤销、重做等功能,这反映出各个命令之间是平等关系;而策略设计模式的各个策略之间可能是先后顺序关系。

原生使用

下面这些是 JavaScript 中常见的原生部分,它们在某种程度上使用到了命令模式的思想和机制。通过封装行为成具体的对象并在需要时进行调用,这些原生功能可以提供更灵活、可扩展的方式来处理相关的请求或操作。

  • 事件处理:JavaScript 中的事件处理机制可以看作是一种命令模式的应用。当用户与页面进行交互时,例如点击按钮、键盘按键或鼠标移动等,事件被触发并执行相应的处理函数。这里事件就充当了命令对象,而事件处理函数则扮演着命令的接收者。

  • XMLHttpRequest 对象:在早期的 Ajax 开发中,我们常使用 XMLHttpRequest 对象来进行异步请求。开发者可以将每个请求封装成一个对象,并通过调用 send() 方法来发送请求。这里的 XMLHttpRequest 对象和 send() 方法即可看作是命令模式的实现,发送请求的行为被封装成具体的命令对象。

  • History API:浏览器的 History API 提供了对浏览器历史记录的控制。通过调用 pushState() 或 replaceState() 方法,我们可以添加或替换浏览器的历史记录条目,并关联相应的状态数据。这里的 pushState() 和 replaceState() 方法可以看作是命令对象,用于执行添加或替换历史记录的操作。

  • document.execCommand():Document 对象的 execCommand() 方法允许在网页中执行命令式的编辑操作,如粘贴、剪切、加粗、斜体等。开发者可以调用 execCommand() 方法并传递相应的命令参数来执行这些操作,从而实现富文本编辑功能。

  • setTimeout() 和 setInterval():JavaScript 提供了 setTimeout() 和 setInterval() 函数来实现定时器功能。开发者可以使用这两个函数将一段代码封装成一个函数对象,并在指定的时间间隔后执行相应的代码,相当于将定时器行为封装成具体的命令对象。

业务实践:

  • 按钮和用户交互:当你需要实现一个具有撤销、重做或记录操作历史的按钮交互功能时,可以使用命令模式。每个按钮可以表示一个命令对象,按下按钮时执行相应的命令操作。

也就是说但凡见到按钮,都可以使用命令设计模式。

  • 异步请求管理:当你需要对异步请求进行批处理、队列化或延迟执行时,命令模式可以很好地组织和管理这些请求。将每个请求封装成一个命令对象,并使用命令队列来依次执行这些命令。

  • 菜单和快捷键:当你需要实现复杂的菜单系统或支持快捷键操作时,命令模式可以帮助你处理不同的菜单项或快捷键动作。每个菜单项或快捷键可以关联一个命令对象,触发时执行相应的命令操作。

  • 动画控制:当你需要控制页面元素的复杂动画序列或状态切换时,命令模式可以提供一种有效的方式。每个动画或状态切换可以封装成一个命令对象,通过调用者来触发执行。

  • 历史记录与撤销:当你需要实现撤销和重做功能或记录用户操作历史时,命令模式非常有用。每个用户操作可以表示一个命令对象,并在执行时更新状态或记录操作,以便支持撤销和重做操作。

以上就是JS设计模式之命令模式的用法详解的详细内容,更多关于JS命令模式的资料请关注本站其它相关文章!

作者最新文章