深入理解JavaScript设计模式之命令模式

发布于:2025-07-09 ⋅ 阅读:(13) ⋅ 点赞:(0)

深入理解JavaScript设计模式之命令模式

深入理解JavaScript设计模式之命令模式

定义

命令模式也是设计模式种相对于变焦简单容易理解的一种设计模式。

JavaScript中,命令模式用于将一个请求或简单操作封装为一个对象。这使得你可以使用不同的请求、队列请求或者记录请求日志、撤销操作等。命令模式通常用于实现诸如撤销/重做功能、事务系统以及在复杂对象间传递请求等场景。

白话说就是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。使得请求发送者和请求接收者能够消除彼此之间的耦合关系,命令模式还支持撤销、排队等操作。

简单命令模式

定义很难了解命令模式的用处,举个开灯关灯的命令模式例子,如下,定义了两个LightOnCommand LightOffCommand 两个命令分别执行light 对象中的onoff方法,在new LightOnCommand的时候将light作为参数传入并执行LightOnCommand.execute();的方法实现开灯关灯的操作。

<body>
  <button id="btn">按钮</button>
</body>
<script>
  /**
   * 点击按钮,执行开灯关灯的操作
   */
  const light = {
    on() {
      console.log("开灯");
    },
    off() {
      console.log("关灯");
    },
  };
  class LightOnCommand {
    constructor(light) {
      this.light = light;
    }
    execute() {
      this.light.on();
    }
  }

  class LightOffCommand {
    constructor(light) {
      this.light = light;
    }
    execute() {
      this.light.off();
    }
  }
  const onLight = new LightOnCommand(light);
  const offLight = new LightOffCommand(light);
  let isOn = true;
  document.getElementById("btn").addEventListener("click", function () {
    (isOn ? onLight : offLight).execute();
    isOn = !isOn;
  });
</script>

深入理解JavaScript设计模式之命令模式
一开始学的时候觉得这就是脱裤子放屁多此一举,但是仔细还差与思考,使用这种方式,可以让代码更加模块化与更容易维护。

  • 解耦:调用者和接收者之间解耦,调用者不需要知道接收者的具体实现。
  • 扩展性:可以很容易地添加新的命令而不需要修改现有的类。
  • 可撤销操作:可以通过记录命令的历史来实现撤销操作。
  • 队列请求:可以将命令存储在队列中,按顺序执行。
  • 日志记录:可以记录命令的历史,便于调试和回溯。

组合命令模式

第一个简单的例子可以看到点击按钮只执行了一次命令,如果有多条命令,那就可以将多个命令添加到Command 里的一个stack 数组中,最后执行Command.execute的时候遍历stack数组中的命令统一遍历执行。
页面中有一个按钮 #btn,当点击按钮时,依次执行以下三个命令:

  1. 开灯(LightOnCommand)
  2. 工人开始工作并停止(WorkerCommand)
  3. 关灯(LightOffCommand)

这些命令被添加到一个 Command 对象中,并在点击事件发生时统一执行,代码如下:

<body>
  <button id="btn">按钮</button>
</body>
<script>
  class Command {
    constructor() {
      this.stack = [];
    }
    add(command) {
      this.stack.push(command);
    }
    execute() {
      this.stack.forEach((command) => command.execute());
    }
  }
  const light = {
    on: () => console.log("开灯"),
    off: () => console.log("关灯"),
  };
  const worker = {
    do: () => console.log("开始工作"),
    stop: () => console.log("停止工作"),
  };

  class WorkerCommand {
    constructor(worker) {
      this.worker = worker;
    }
    execute() {
      this.worker.do();
      this.worker.stop();
    }
  }
  // 命令拆分
  class LightOnCommand {
    constructor(light) {
      this.light = light;
    }
    execute() {
      this.light.on();
    }
  }
  class LightOffCommand {
    constructor(light) {
      this.light = light;
    }
    execute() {
      this.light.off();
    }
  }
  const command = new Command();
  command.add(new LightOnCommand(light));
  command.add(new WorkerCommand(worker));
  command.add(new LightOffCommand(light));
  document.getElementById("btn").addEventListener("click", () => {
    command.execute();
  });
</script>

深入理解JavaScript设计模式之命令模式
这种写法的优点:

  1. 解耦调用者与执行者 按钮点击事件(调用者)并不直接调用 light.on()worker.do(),而是交给命令对象去处理。 这样使得界面逻辑和业务逻辑分离,提高了可维护性。
  2. 易于扩展新的命令 如果需要新增功能,比如“打开风扇”或“播放音乐”,只需要定义一个新的命令类并加入命令队列即可,不需要修改已有代码。 符合 开放封闭原则(OCP):对扩展开放,对修改关闭。
  3. 支持组合命令 Command类中的 stack 可以保存多个命令,可以轻松实现宏命令(一组命令的集合),如示例中的一键执行开灯、工作、关灯等操作。 后续也可以支持撤销/重做等功能(只需记录历史栈)。
  4. 便于测试与复用 每个命令是独立的对象,可以单独测试其 execute() 方法。 命令可以在不同上下文中复用,例如在定时器中触发、远程调用等。
  5. 提升代码可读性和结构清晰度 将每个操作抽象为类,有助于理解意图(Intent)。 比如看到 new LightOnCommand(light),就知道这是“开灯”的命令,比直接调用函数更具语义化。

总的来说:通过组合命令模式可以实现良好的职责分离,灵活扩展和统一控制,如果需求遇到了对多个操作进行封装调度记录和撤销的时候,可以使用组合命令实现。

使用命令模式实现文本编辑器

如下举例加深命令模式的使用,如下我想实现一个文本编辑器,其中功能有【清空内容转为大写转为小写撤销重做指令列表
深入理解JavaScript设计模式之命令模式

目标

实现一个基于命令模式的文本编辑器,具备【清空内容转为大写转为小写撤销重做指令列表显示每一步操作的命令记录

关键类说明

  • Editor(接收者):
class Editor {
  constructor() {
    this.content = "";
  }
}

存储当前文本内容,所用命令的实际执行者。

  • TextChangeCommand(基础命令)
class TextChangeCommand {
 constructor(editor, newText) {
    this.editor = editor;
    this.newText = newText;
    this.previousText = editor.content;
  }
  execute() {
    this.editor.content = this.newText;
  }
  undo() {
    this.editor.content = this.previousText;
  }
}

表示每次文本输入变更的操作,记录修改前后的状态,支持撤销。

  • CommandManager(扩展命令)
class CommandManager {
   constructor() {
      this.tack = [];
    }
    execute(command) {
      if (command) {
        this.tack.push(command);
        command.execute();
        updateUI();
      }
    }
    // 清空
    redo() {
      this.tack = [];
      updateUI();
    }
    // 撤销
    undo() {
      if (this.tack.length > 0) {
        const command = this.tack.pop();
        command.undo();
        updateUI();
      } else {
        console.log("没有可撤销的命令");
        updateUI();
        return;
      }
    }
    // 查看命令列表
    getTackList() {
      return this.tack;
    }
  }

使用栈(tack)保存所有已执行命令,提供 execute()undo()redo()getTackList() 方法,控制整个命令流程。

  • UpperCaseCommand(命令管理器)
class UpperCaseCommand {
constructor(editor) {
    this.editor = editor;
    this.previousText = editor.content;
    this.newText = editor.content.toUpperCase();
  }
  execute() {
    this.editor.content = this.newText;
  }
  undo() {
    this.editor.content = this.previousText;
  }
}

将文本转为大写的命令,同样支持撤销,可以继续扩展更多命令如 LowerCaseCommand,ClearCommand 等。

实现的效果

深入理解JavaScript设计模式之命令模式

交互逻辑流程

  1. 初始化
    创建 EditorCommandManager,设置初始文本为空,绑定 DOM 元素(如 textarea、按钮)。
  2. 用户操作触发命令,输入文字 → 触发input事件 → 创建TextChangeCommand → 执行并入栈
    点击按钮(清空、大写、小写)→ 创建对应命令 → 执行并入栈。
  3. 撤销 / 重做,“撤销”点击 → 从栈中弹出最后一个命令 → 调用 .undo(),“重做”点击 → 清空栈(当前简单实现) 当前重做只是清空栈,没有真正实现“恢复撤销”的动作,可进一步改进。

所有代码:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>命令模式文本编辑器</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background: linear-gradient(45deg, #ff6b6b, #c471ad);
        font-family: Arial, sans-serif;
      }
      .editor-container {
        width: 300px;
        background: white;
        border-radius: 5px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      .header {
        background: #2c3e50;
        color: white;
        text-align: center;
        padding: 10px 0;
        border-top-left-radius: 5px;
        border-top-right-radius: 5px;
      }
      .content {
        padding: 20px;
        height: 150px;
        border-bottom: 1px solid #ddd;
      }
      .buttons {
        display: flex;
        justify-content: space-around;
        padding: 10px;
      }
      .buttons button {
        padding: 8px 15px;
        border: none;
        border-radius: 3px;
        cursor: pointer;
      }
      .buttons .primary {
        background: #3498db;
        color: white;
      }
      .buttons .secondary {
        background: #ecf0f1;
        color: #333;
      }
      #contentText {
        border: none;
        height: 150px;
        width: 100%;
      }
      #tackListView {
        border: none;
        height: 130px;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div class="editor-container">
      <div class="header">命令模式文本编辑器</div>
      <div class="content">
        <!-- 文本编辑区域 -->
        <textarea id="contentText"></textarea>
      </div>
      <div class="buttons">
        <button class="primary" id="clearBtn">清空内容</button>
        <button class="primary" id="upperBtn">转为大写</button>
        <button class="primary" id="lowerBtn">转为小写</button>
      </div>
      <div class="buttons">
        <button class="secondary" id="undoBtn">撤销</button>
        <button class="secondary" id="redoBtn">重做</button>
        <button class="secondary" id="stackList">指令列表</button>
      </div>
      <div class="content">
        命令列表:
        <textarea id="tackListView"></textarea>
      </div>
    </div>
    <script>
      class TextChangeCommand {
        constructor(editor, newText) {
          this.editor = editor;
          this.newText = newText;
          this.previousText = editor.content;
        }
        execute() {
          this.editor.content = this.newText;
        }
        undo() {
          this.editor.content = this.previousText;
        }
      }

      class CommandManager {
        constructor() {
          this.tack = [];
        }
        execute(command) {
          if (command) {
            this.tack.push(command);
            command.execute();
            updateUI();
          }
        }
        // 清空
        redo() {
          this.tack = [];
          updateUI();
        }
        // 撤销
        undo() {
          if (this.tack.length > 0) {
            const command = this.tack.pop();
            command.undo();
            updateUI();
          } else {
            console.log("没有可撤销的命令");
            updateUI();
            return;
          }
        }
        // 查看命令列表
        getTackList() {
          return this.tack;
        }
      }

      class UpperCaseCommand {
        constructor(editor) {
          this.editor = editor;
          this.previousText = editor.content;
          this.newText = editor.content.toUpperCase();
        }
        execute() {
          this.editor.content = this.newText;
        }
        undo() {
          this.editor.content = this.previousText;
        }
      }
      // 接收者
      class Editor {
        constructor() {
          this.content = "";
        }
      }
      // 初始化
      const editor = new Editor();
      const commandManager = new CommandManager();
      // DOM元素
      const textarea = document.getElementById("contentText");
      // 设置初始内容
      editor.content = textarea.value;

      // 事件监听
      textarea.addEventListener("input", function () {
        const command = new TextChangeCommand(editor, textarea.value);
        commandManager.execute(command);
      });

      document
        .getElementById("clearBtn")
        .addEventListener("click", function () {
          const command = new TextChangeCommand(editor, "");
          commandManager.execute(command);
        });
      document
        .getElementById("upperBtn")
        .addEventListener("click", function () {
          const command = new UpperCaseCommand(editor);
          commandManager.execute(command);
        });
      document
        .getElementById("lowerBtn")
        .addEventListener("click", function () {
          const command = new TextChangeCommand(
            editor,
            textarea.value.toLowerCase()
          );
          commandManager.execute(command);
        });
      document.getElementById("undoBtn").addEventListener("click", function () {
        commandManager.undo();
      });
      document.getElementById("redoBtn").addEventListener("click", function () {
        const command = new TextChangeCommand(editor, "");
        commandManager.execute(command);
        commandManager.redo();
      });
      document
        .getElementById("stackList")
        .addEventListener("click", function () {
          console.log(commandManager.getTackList());
        });

      // 更新UI
      function updateUI() {
        // 更新主文本区域
        textarea.value = editor.content;

        // 获取命令列表显示区域
        const tackListView = document.getElementById("tackListView");

        // 获取当前命令栈
        const commands = commandManager.getTackList();

        // 格式化命令记录
        let logText = "";
        for (let i = 0; i < commands.length; i++) {
          const cmd = commands[i];
          if (cmd instanceof TextChangeCommand) {
            logText += `${i + 1}. 文本修改为: ${cmd.newText}\n`;
          } else if (cmd instanceof UpperCaseCommand) {
            logText += `${i + 1}. 转为大写: ${cmd.newText}\n`;
          }
        }

        // 如果没有命令,显示提示信息
        if (commands.length === 0) {
          logText = "暂无命令记录";
        }

        // 更新命令列表显示区域
        tackListView.value = logText;
      }
      // 初始化UI更新
      updateUI();
    </script>
  </body>
</html>

总结

设计模式不是“炫技”,而是"沉淀",希望通过阅读和学习《JavaScript设计模式》和实践中,在显示业务需求开发中写出更具有可维护性,可扩展性的代码。

致敬—— 《JavaScript设计模式》· 曾探


网站公告

今日签到

点亮在社区的每一天
去签到