JavaScript状态模式实战:从电灯控制到文件上传
本文详细解析JavaScript中的状态模式,通过电灯开关和文件上传两个实际案例,展示如何将复杂的状态逻辑封装为独立类。文章指出了传统条件分支代码的缺陷,并演示了如何利用状态模式简化代码结构、提高可维护性,同时探讨了抽象父类在状态模式中的关键作用。
1 什么是状态模式
状态模式的核心定义是:当一个对象的内部状态发生改变时,允许其改变行为,使得该对象看起来似乎修改了它的类。
下面通过一个具体场景来说明:
- 假设有一盏电灯,电灯上仅配备了一个开关按钮。
- 在电灯处于开启状态时,按下开关,电灯会切换至关闭状态;再次按下开关,电灯又会重新开启。
- 可见,同一个开关按钮在不同的状态下,所表现出的行为是不同的。
我们可以用代码来描述上述场景:
// 定义一个Light类
var Light = function () {
this.state = "off"; // 给电灯设置初始状态 off
this.button = null; // 电灯开关按钮
};
// 在页面中创建一个真实的button节点
Light.prototype.init = function () {
var button = document.createElement("button"), // 创建一个开关按钮
self = this;
button.innerHTML = "开关";
this.button = document.body.appendChild(button);
// 开关被按下的事件
this.button.onclick = function () {
self.buttonWasPressed();
};
};
// 开关被按下的行为
Light.prototype.buttonWasPressed = function () {
// 如果当前是关灯状态,按下开关表示开灯
if (this.state === "off") {
console.log("开灯");
this.state = "on";
} else if (this.state === "on") {
// 如果当前是开灯状态,按下开关表示关灯
console.log("关灯");
this.state = "off";
}
};
var light = new Light();
light.init();然而,电灯的品种繁多。另一种电灯虽然也只有一个开关,但其行为逻辑不同:第一次按下开启弱光,第二次按下开启强光,第三次按下才关闭电灯。现在我们需要改造上述代码,以支持这种新型电灯的制造:
Light.prototype.buttonWasPressed = function () {
if (this.state === "off") {
console.log("弱光");
this.state = "weakLight";
} else if (this.state === "weakLight") {
console.log("强光");
this.state = "strongLight";
} else if (this.state === "strongLight") {
console.log("关灯");
this.state = "off";
}
};在上述代码实现中,存在几个明显的缺点:
buttonWasPressed方法违反了开放—封闭原则。每次新增或修改灯光的状态时,都需要改动buttonWasPressed方法中的代码,这使得该方法变得非常不稳定。- 所有与状态相关的行为都被封装在
buttonWasPressed方法中。如果该电灯增加了其他光的种类,该方法将会变得越来越庞大。 - 状态的切换过程不明显,仅仅表现为改变
state的值,这容易导致某些状态被遗漏。 - 状态之间的切换关系依赖于
if、else语句。增加或修改一个状态可能需要改变若干个操作,这使得代码难以阅读和维护。
2 使用状态模式改造电灯程序
状态模式的关键在于将事物的每种状态都封装成单独的类,与某种状态相关的行为都被封装在这个类的内部。因此,当button被按下时,只需要在上下文中把请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。

同时,我们还可以把状态的切换规则事先分布在状态类中,这样就有效地消除了原本存在的
大量条件分支语句,代码如下:
// OffLightState:
var OffLightState = function (light) {
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
console.log("弱光"); // offLightState 对应的行为
this.light.setState(this.light.weakLightState); // 切换状态到 weakLightState
};
// WeakLightState:
var WeakLightState = function (light) {
this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
console.log("强光"); // weakLightState 对应的行为
this.light.setState(this.light.strongLightState); // 切换状态到 strongLightState
};
// StrongLightState:
var StrongLightState = function (light) {
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
console.log("关灯"); // strongLightState 对应的行为
this.light.setState(this.light.offLightState); // 切换状态到 offLightState
};
// 改写Light类,在Light类中为每个状态类都创建一个状态对象,可以很明显的看到灯的种类
var Light = function () {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
// 按下按钮的事件中,将请求委托给当前持有的状态对象去执行
Light.prototype.init = function () {
var button = document.createElement("button"), // 创建button
self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = "开关";
// 设置当前状态
this.currState = this.offLightState;
this.button.onclick = function () {
self.currState.buttonWasPressed();
};
};
// 切换light对象的状态
Light.prototype.setState = function (newState) {
this.currState = newState;
};
var light = new Light();
light.init();3 缺少抽象类的变通方式
在上面的代码中,状态类中定义了一些共同的行为方法,Context最终会将请求委托给状态对象的这些方法,在这个例子里这个方法就是buttonWasPressed。无论增加了多少种状态类,它们都必须实现buttonWasPressed方法。
因此,在使用状态模式时要格外小心。如果我们编写一个状态子类时,忘记了给这个状态子类实现buttonWasPressed方法,则会在状态切换的时候抛出异常,因为Context总是把请求委托给状态对象的buttonWasPressed方法。
因此我们让抽象父类的抽象方法直接抛出一个异常:
var State = function () {};
State.prototype.buttonWasPressed = function () {
throw new Error("父类的 buttonWasPressed 方法必须被重写");
};
var SuperStrongLightState = function (light) {
this.light = light;
};
SuperStrongLightState.prototype = new State(); // 继承抽象父类
SuperStrongLightState.prototype.buttonWasPressed = function () {
// 重写 buttonWasPressed 方法
console.log("关灯");
this.light.setState(this.light.offLightState);
};4 示例:文件上传
4.1 场景描述
例如,控制文件上传需要两个节点按钮,第一个用于暂停和继续上传,第二个用于删除文件。
- 当文件处于扫描状态时,不能进行任何操作,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的
md5值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态。 - 上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传。
- 扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。
假设我们使用一个插件对象帮助我们完成上传工作:
var plugin = (function () {
var plugin = document.createElement("embed");
plugin.style.display = "none";
plugin.type = "application/txftn-webkit";
plugin.sign = function () {
console.log("开始文件扫描");
};
plugin.pause = function () {
console.log("暂停文件上传");
};
plugin.uploading = function () {
console.log("开始文件上传");
};
plugin.del = function () {
console.log("删除文件上传");
};
plugin.done = function () {
console.log("文件上传完成");
};
document.body.appendChild(plugin);
return plugin;
})();上传是一个异步的过程,所以控件会不停地调用全局函数window.external.upload,来通知目前的上传进度,控件会把当前的文件状态作为参数state塞进window.external.upload,在此例中该函数负责打印一些log:
window.external.upload = function (state) {
console.log(state); // 可能为 sign、uploading、done、error
};4.2 代码过程
首先定义Upload类,在构造函数中为每种状态子类都创建一个实例对象:
var Upload = function (fileName) {
this.plugin = plugin;
this.fileName = fileName;
this.button1 = null;
this.button2 = null;
this.signState = new SignState(this); // 设置初始状态为 waiting
this.uploadingState = new UploadingState(); // 上传中
this.pauseState = new PauseState(this); // 暂停
this.doneState = new DoneState(this); // 上传完成
this.errorState = new ErrorState(this); // 上传错误
this.currState = this.signState; // 设置当前状态
};创建两个按钮,一个控制文件暂停和继续上传,一个用于删除文件:
Upload.prototype.init = function () {
var that = this;
this.dom = document.createElement("div");
this.dom.innerHTML =
"<span>文件名称:" +
this.fileName +
'</span><button data-action="button1">扫描中</button><button data-action="button2">删除</button>';
document.body.appendChild(this.dom);
this.button1 = this.dom.querySelector('[data-action="button1"]'); // 第一个按钮
this.button2 = this.dom.querySelector('[data-action="button2"]'); // 第二个按钮
this.bindEvent();
};为两个按钮分别绑定点击事件,在点击了按钮之后,Context并不做任何具体的操作,而是把请求委托给当前的状态类来执行:
Upload.prototype.bindEvent = function () {
var self = this;
this.button1.onclick = function () {
self.currState.clickHandler1();
};
this.button2.onclick = function () {
self.currState.clickHandler2();
};
};
// 扫描中
Upload.prototype.sign = function () {
this.plugin.sign();
this.currState = this.signState;
};
// 上传中
Upload.prototype.uploading = function () {
this.button1.innerHTML = "正在上传,点击暂停";
this.plugin.uploading();
this.currState = this.uploadingState;
};
// 暂停
Upload.prototype.pause = function () {
this.button1.innerHTML = "已暂停,点击继续上传";
this.plugin.pause();
this.currState = this.pauseState;
};
// 上传成功
Upload.prototype.done = function () {
this.button1.innerHTML = "上传完成";
this.plugin.done();
this.currState = this.doneState;
};
// 上传失败
Upload.prototype.error = function () {
this.button1.innerHTML = "上传失败";
this.currState = this.errorState;
};
// 删除
Upload.prototype.del = function () {
this.plugin.del();
this.dom.parentNode.removeChild(this.dom);
};再接下来是编写各个状态类的实现:
var StateFactory = (function () {
var State = function () {};
State.prototype.clickHandler1 = function () {
throw new Error("子类必须重写父类的 clickHandler1 方法");
};
State.prototype.clickHandler2 = function () {
throw new Error("子类必须重写父类的 clickHandler2 方法");
};
return function (param) {
var F = function (uploadObj) {
this.uploadObj = uploadObj;
};
F.prototype = new State();
for (var i in param) {
F.prototype[i] = param[i];
}
return F;
};
})();
var SignState = StateFactory({
clickHandler1: function () {
console.log("扫描中,点击无效...");
},
clickHandler2: function () {
console.log("文件正在上传中,不能删除");
},
});
var UploadingState = StateFactory({
clickHandler1: function () {
this.uploadObj.pause();
},
clickHandler2: function () {
console.log("文件正在上传中,不能删除");
},
});
var PauseState = StateFactory({
clickHandler1: function () {
this.uploadObj.uploading();
},
clickHandler2: function () {
this.uploadObj.del();
},
});
var DoneState = StateFactory({
clickHandler1: function () {
console.log("文件已完成上传, 点击无效");
},
clickHandler2: function () {
this.uploadObj.del();
},
});
var ErrorState = StateFactory({
clickHandler1: function () {
console.log("文件上传失败, 点击无效");
},
clickHandler2: function () {
this.uploadObj.del();
},
});测试一下:
var uploadObj = new Upload("AAAAAAAAA");
uploadObj.init();
window.external.upload = function (state) {
// 插件调用 JavaScript 的方法
uploadObj[state]();
};
window.external.upload("sign"); // 文件开始扫描
setTimeout(function () {
window.external.upload("uploading"); // 1 秒后开始上传
}, 1000);
setTimeout(function () {
window.external.upload("done"); // 5 秒后上传完成
}, 5000);
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持本站。
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
