本文转载自微信公众号「JS每日一题」,作者灰灰 。转载本文请联系JS每日一题公众号。
一、是什么
命令模式是最简单和优雅的模式之一,命令模式中的命令指的是一个执行某些特定事情的指令
该模式旨在将函数的调用、请求和操作封装成为一个单一的对象
请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令
例如在一个快餐店,用户向服务员点餐。服务员将用户的需求记录在清单上:
- 请求者点菜:参数是菜名(我要什么菜),时间(什么时候要),该需求封装起来后,如果有变化我可以修改参数
- 命令模式将点餐内容封装成为命令对象,命令对象就是填写的清单
- 用户不知道接收者(厨师)是谁,也不知道厨师的炒菜方式与步骤
- 请求者可以要求修改命令执行时间,例如晚1小时再要
二、实现
命令模式由三种角色构成:
- 发布者 invoker(发出命令,调用命令对象,不知道如何执行与谁执行)
- 接收者 receiver (提供对应接口处理请求,不知道谁发起请求)
- 命令对象 command(接收命令,调用接收者对应接口处理发布者的请求)

实现代码如下:
class Receiver { // 接收者类
execute() {
console.log('接收者执行请求');
}
}
class Command { // 命令对象类
constructor(receiver) {
this.receiver = receiver;
}
execute () { // 调用接收者对应接口执行
console.log('命令对象->接收者->对应接口执行');
this.receiver.execute();
}
}
class Invoker { // 发布者类
constructor(command) {
this.command = command;
}
invoke() { // 发布请求,调用命令对象
console.log('发布者发布请求');
this.command.execute();
}
}
const warehouse = new Receiver(); // 厨师
const order = new Command(warehouse); // 订单
const client = new Invoker(order); // 请求者
client.invoke();
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
三、应用场景
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时,希望用一种松耦合的方式来设计程序,使的请求发送者和请求接收者能够消除彼此之间的耦合关系
菜单
现在我们需要实现一个界面,包含很多个按钮。每个按钮有不同的功能,我们利用命令模式来完成
<button id="button1"></button>
<button id="button2"></button>
<button id="button3"></button>
<script>
var button1 = document.getElementById("button1");
var button2 = document.getElementById("button2");
var button3 = document.getElementById("button3");
</script>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
然后定义一个setCommand函数,负责将按钮安装命令,可以确定的是,点击按钮会执行某个 command 命令,执行命令的动作被约定为调用 command 对象的 execute() 方法。如下:
var button1 = document.getElementById('button1')
var setCommand = function(button, conmmand) {
button.onclick = function() {
conmmand.execute()
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
点击按钮之后具体行为包括刷新菜单界面、增加子菜单和删除子菜单等,这几个功能被分布在 MenuBar 和 SubMenu 这两个对象中:
var MenuBar = {
refresh: function() {
console.log('刷新菜单目录')
}
}
var SubMenu = {
add: function() {
console.log('增加子菜单')
},
del: function(){
console.log('删除子菜单');
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
这些功能需要封装在对应的命令类中:
// 刷新菜单目录命令类
class RefreshMenuBarCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
// 增加子菜单命令类
class AddSubMenuCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
// '删除子菜单命令类
class DelSubMenuCommand {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.refresh();
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面:
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
撤销
命令模式的作用不仅是封装运算块,而且可以很方便地给命令对象增加撤销操作
页面中有一个 input 文本框和一个 button 按钮,文本框中可以输入一些数字,表示小球移动后的水平位置,小球在用户点击按钮后立刻开始移动,如下:
<div
id="ball"
style="position: absolute; background: #000; width: 50px; height: 50px"
></div>
输入小球移动后的位置:<input id="pos" />
<button id="moveBtn">开始移动</button>
<script>
var ball = document.getElementById("ball");
var pos = document.getElementById("pos");
var moveBtn = document.getElementById("moveBtn");
moveBtn.onclick = function () {
var animate = new Animate(ball);
animate.start("left", pos.value, 1000, "strongEaseOut");
};
</script>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
换成命令模式如下:
var ball = document.getElementById("ball");
var pos = document.getElementById("pos");
var moveBtn = document.getElementById("moveBtn");
var MoveCommand = function (receiver, pos) {
this.receiver = receiver;
this.pos = pos;
};
MoveCommand.prototype.execute = function () {
this.receiver.start("left", this.pos, 1000, "strongEaseOut");
};
var moveCommand;
moveBtn.onclick = function () {
var animate = new Animate(ball);
moveCommand = new MoveCommand(animate, pos.value);
moveCommand.execute();
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
撤销操作的实现一般是给命令对象增加一个名为 unexecude 或者 undo的方法,在该方法里执行 execute 的反向操作
在 command.execute 方法让小球开始真正运动之前,需要先记录小球的当前位置,在 unexecude 或者 undo 操作中,再让小球回到刚刚记录下的位置,代码如下:
class MoveCommand {
constructor(receiver, pos) {
this.receiver = receiver;
this.pos = pos;
this.oldPos = null;
}
execute() {
this.receiver.start('left', this.pos, 1000, 'strongEaseOut');
this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName]; // 记录小球开始移动前的位置
}
undo() {
this.receiver.start('left', this.oldPos, 1000, 'strongEaseOut'); // 回到小球移动前记录的位置
}
}
var moveCommand;
moveBtn.onclick = function () {
var animate = new Animate(ball);
moveCommand = new MoveCommand(animate, pos.value); moveCommand.execute();
};
cancelBtn.onclick = function () {
moveCommand.undo();// 撤销命令
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
现在通过命令模式轻松地实现了撤销功能。如果用普通方法调用来实现,也许需要每次都手工记录小球的运动轨迹,才能让它还原到之前的位置
而命令模式中小球的原始位置在小球开始移动前已经作为 command 对象的属性被保存起来,所以只需要再提供一个 undo 方法,并且在 undo方法中让小球会到刚刚记录的原始位置就可以
参考文献
https://www.runoob.com/design-pattern/command-pattern.html
https://juejin.cn/post/6844903673697402888
https://juejin.cn/post/6995474681813811208