深入理解JavaScript設計模式之命令模式
文章目錄
- 深入理解JavaScript設計模式之命令模式
- 定義
- 簡單命令模式
- 組合命令模式
- 使用命令模式實現文本編輯器
- 目標
- 關鍵類說明
- 實現的效果
- 交互邏輯流程
- 所有代碼:
- 總結
定義
命令模式也是設計模式種相對于變焦簡單容易理解的一種設計模式。
在
JavaScript
中,命令模式用于將一個請求或簡單操作封裝為一個對象。這使得你可以使用不同的請求、隊列請求或者記錄請求日志、撤銷操作等。命令模式通常用于實現諸如撤銷/重做功能、事務系統以及在復雜對象間傳遞請求等場景。
白話說就是:有時候需要向某些對象發送請求,但是并不知道請求的接收者是誰,也不知道被請求的操作是什么。使得請求發送者和請求接收者能夠消除彼此之間的耦合關系,命令模式還支持撤銷、排隊等操作。
簡單命令模式
定義很難了解命令模式的用處,舉個開燈關燈的命令模式例子,如下,定義了兩個LightOnCommand
與LightOffCommand
兩個命令分別執行light
對象中的on
與off
方法,在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>
一開始學的時候覺得這就是脫褲子放屁多此一舉,但是仔細還差與思考,使用這種方式,可以讓代碼更加模塊化與更容易維護。
- 解耦:調用者和接收者之間解耦,調用者不需要知道接收者的具體實現。
- 擴展性:可以很容易地添加新的命令而不需要修改現有的類。
- 可撤銷操作:可以通過記錄命令的歷史來實現撤銷操作。
- 隊列請求:可以將命令存儲在隊列中,按順序執行。
- 日志記錄:可以記錄命令的歷史,便于調試和回溯。
組合命令模式
第一個簡單的例子可以看到點擊按鈕只執行了一次命令,如果有多條命令,那就可以將多個命令添加到Command
里的一個stack
數組中,最后執行Command.execute
的時候遍歷stack
數組中的命令統一遍歷執行。
頁面中有一個按鈕 #btn
,當點擊按鈕時,依次執行以下三個命令:
- 開燈(LightOnCommand)
- 工人開始工作并停止(WorkerCommand)
- 關燈(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>
這種寫法的優點:
- 解耦調用者與執行者 按鈕點擊事件(調用者)并不直接調用
light.on()
或worker.do()
,而是交給命令對象去處理。 這樣使得界面邏輯和業務邏輯分離,提高了可維護性。- 易于擴展新的命令 如果需要新增功能,比如“打開風扇”或“播放音樂”,只需要定義一個新的命令類并加入命令隊列即可,不需要修改已有代碼。 符合 開放封閉原則
(OCP)
:對擴展開放,對修改關閉。- 支持組合命令
Command
類中的stack
可以保存多個命令,可以輕松實現宏命令(一組命令的集合),如示例中的一鍵執行開燈、工作、關燈等操作。 后續也可以支持撤銷/重做等功能(只需記錄歷史棧)。- 便于測試與復用 每個命令是獨立的對象,可以單獨測試其
execute()
方法。 命令可以在不同上下文中復用,例如在定時器中觸發、遠程調用等。- 提升代碼可讀性和結構清晰度 將每個操作抽象為類,有助于理解意圖
(Intent)
。 比如看到new LightOnCommand(light)
,就知道這是“開燈”的命令,比直接調用函數更具語義化。
總的來說:通過組合命令模式可以實現良好的職責分離,靈活擴展和統一控制,如果需求遇到了對多個操作進行封裝調度記錄和撤銷的時候,可以使用組合命令實現。
使用命令模式實現文本編輯器
如下舉例加深命令模式的使用,如下我想實現一個文本編輯器,其中功能有【清空內容
、轉為大寫
、轉為小寫
、撤銷
、重做
、指令列表
】
目標
實現一個基于命令模式的文本編輯器,具備【清空內容
、轉為大寫
、轉為小寫
、撤銷
、重做
、指令列表
,顯示每一步操作的命令記錄
】
關鍵類說明
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
等。
實現的效果
交互邏輯流程
- 初始化
創建Editor
和CommandManager
,設置初始文本為空,綁定DOM
元素(如textarea
、按鈕)。 - 用戶操作觸發命令,輸入文字 → 觸發
input
事件 → 創建TextChangeCommand
→ 執行并入棧
點擊按鈕(清空、大寫、小寫)→ 創建對應命令 → 執行并入棧。 - 撤銷 / 重做,“撤銷”點擊 → 從棧中彈出最后一個命令 → 調用
.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());});// 更新UIfunction 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設計模式》· 曾探