JavaScript中的狀態(tài)模式詳解
1 什么是狀態(tài)模式
允許一個(gè)對(duì)象在其內(nèi)部狀態(tài)改變時(shí)改變它的行為,對(duì)象看起來(lái)似乎修改了它的類(lèi)。
比如說(shuō)這樣一個(gè)場(chǎng)景:
- 有一個(gè)電燈,電燈上面只有一個(gè)開(kāi)關(guān)。
- 當(dāng)電燈開(kāi)著的時(shí)候,此時(shí)按下開(kāi)關(guān),電燈會(huì)切換到關(guān)閉狀態(tài);再按一次開(kāi)關(guān),電燈又將被打開(kāi)。
- 同一個(gè)開(kāi)關(guān)按鈕,在不同的狀態(tài)下,表現(xiàn)出來(lái)的行為是不一樣的。
我們用代碼來(lái)描述上面的場(chǎng)景:
// 定義一個(gè)Light類(lèi)
var Light = function () {
this.state = "off"; // 給電燈設(shè)置初始狀態(tài) off
this.button = null; // 電燈開(kāi)關(guān)按鈕
};
// 在頁(yè)面中創(chuàng)建一個(gè)真實(shí)的button節(jié)點(diǎn)
Light.prototype.init = function () {
var button = document.createElement("button"), // 創(chuàng)建一個(gè)開(kāi)關(guān)按鈕
self = this;
button.innerHTML = "開(kāi)關(guān)";
this.button = document.body.appendChild(button);
// 開(kāi)關(guān)被按下的事件
this.button.onclick = function () {
self.buttonWasPressed();
};
};
// 開(kāi)關(guān)被按下的行為
Light.prototype.buttonWasPressed = function () {
// 如果當(dāng)前是關(guān)燈狀態(tài),按下開(kāi)關(guān)表示開(kāi)燈
if (this.state === "off") {
console.log("開(kāi)燈");
this.state = "on";
} else if (this.state === "on") {
// 如果當(dāng)前是開(kāi)燈狀態(tài),按下開(kāi)關(guān)表示關(guān)燈
console.log("關(guān)燈");
this.state = "off";
}
};
var light = new Light();
light.init();但是燈的種類(lèi)是多種多樣的,另外一種電燈,這種電燈也只有一個(gè)開(kāi)關(guān),但它的表現(xiàn)是:第一次按下打開(kāi)弱光,第二次按下打開(kāi)強(qiáng)光,第三次才是關(guān)閉電燈,現(xiàn)在我們改造上面的代碼來(lái)完成這種新型電燈的制造:
Light.prototype.buttonWasPressed = function () {
if (this.state === "off") {
console.log("弱光");
this.state = "weakLight";
} else if (this.state === "weakLight") {
console.log("強(qiáng)光");
this.state = "strongLight";
} else if (this.state === "strongLight") {
console.log("關(guān)燈");
this.state = "off";
}
};在上面的代碼中,存在一些很明顯的缺點(diǎn):
buttonWasPressed方法違反開(kāi)放—封閉原則,每次新增或者修改燈光的狀態(tài),都需要改動(dòng)buttonWasPressed方法中的代碼,這使其成為了一個(gè)非常不穩(wěn)定的方法- 所有跟狀態(tài)有關(guān)的行為,都被封裝在
buttonWasPressed方法里,如果這個(gè)電燈又增加了其他光的種類(lèi),那這個(gè)方法會(huì)越來(lái)越龐大 - 狀態(tài)的切換不明顯,僅僅表現(xiàn)為改變
state,容易漏掉某些狀態(tài) - 狀態(tài)之間的切換關(guān)系,是靠
if、else語(yǔ)句,增加或者修改一個(gè)狀態(tài)可能需要改變?nèi)舾蓚€(gè)操作,這使代碼難以閱讀和維護(hù)
2 使用狀態(tài)模式改造電燈程序
狀態(tài)模式的關(guān)鍵是把事物的每種狀態(tài)都封裝成單獨(dú)的類(lèi),跟此種狀態(tài)有關(guān)的行為都被封裝在這個(gè)類(lèi)的內(nèi)部,所以button被按下的的時(shí)候,只需要在上下文中,把這個(gè)請(qǐng)求委托給當(dāng)前的狀態(tài)對(duì)象即可,該狀態(tài)對(duì)象會(huì)負(fù)責(zé)渲染它自身的行為。

同時(shí)我們還可以把狀態(tài)的切換規(guī)則事先分布在狀態(tài)類(lèi)中, 這樣就有效地消除了原本存在的
大量條件分支語(yǔ)句,代碼如下:
// OffLightState:
var OffLightState = function (light) {
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
console.log("弱光"); // offLightState 對(duì)應(yīng)的行為
this.light.setState(this.light.weakLightState); // 切換狀態(tài)到 weakLightState
};
// WeakLightState:
var WeakLightState = function (light) {
this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
console.log("強(qiáng)光"); // weakLightState 對(duì)應(yīng)的行為
this.light.setState(this.light.strongLightState); // 切換狀態(tài)到 strongLightState
};
// StrongLightState:
var StrongLightState = function (light) {
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
console.log("關(guān)燈"); // strongLightState 對(duì)應(yīng)的行為
this.light.setState(this.light.offLightState); // 切換狀態(tài)到 offLightState
};
// 改寫(xiě)Light類(lèi),在Light類(lèi)中為每個(gè)狀態(tài)類(lèi)都創(chuàng)建一個(gè)狀態(tài)對(duì)象,可以很明顯的看到燈的種類(lèi)
var Light = function () {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
// 按下按鈕的事件中,將請(qǐng)求委托給當(dāng)前持有的狀態(tài)對(duì)象去執(zhí)行
Light.prototype.init = function () {
var button = document.createElement("button"), // 創(chuàng)建button
self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = "開(kāi)關(guān)";
// 設(shè)置當(dāng)前狀態(tài)
this.currState = this.offLightState;
this.button.onclick = function () {
self.currState.buttonWasPressed();
};
};
// 切換light對(duì)象的狀態(tài)
Light.prototype.setState = function (newState) {
this.currState = newState;
};
var light = new Light();
light.init();3 缺少抽象類(lèi)的變通方式
在上面的代碼中,在狀態(tài)類(lèi)中將定義一些共同的行為方法,Context最終會(huì)將請(qǐng)求委托給狀態(tài)對(duì)象的這些方法,在這個(gè)例子里這個(gè)方法就是buttonWasPressed。無(wú)論增加了多少種狀態(tài)類(lèi),它們都必須實(shí)現(xiàn)buttonWasPressed方法。
所以使用狀態(tài)模式的時(shí)候要格外小心,如果我們編寫(xiě)一個(gè)狀態(tài)子類(lèi)時(shí),忘記了給這個(gè)狀態(tài)子類(lèi)實(shí)現(xiàn)buttonWasPressed方法,則會(huì)在狀態(tài)切換的時(shí)候拋出異常,因?yàn)?code>Context總是把請(qǐng)求委托給狀態(tài)對(duì)象的buttonWasPressed方法。
因此我們讓抽象父類(lèi)的抽象方法直接拋出一個(gè)異常:
var State = function () {};
State.prototype.buttonWasPressed = function () {
throw new Error("父類(lèi)的 buttonWasPressed 方法必須被重寫(xiě)");
};
var SuperStrongLightState = function (light) {
this.light = light;
};
SuperStrongLightState.prototype = new State(); // 繼承抽象父類(lèi)
SuperStrongLightState.prototype.buttonWasPressed = function () {
// 重寫(xiě) buttonWasPressed 方法
console.log("關(guān)燈");
this.light.setState(this.light.offLightState);
};4 示例:文件上傳
4.1 場(chǎng)景描述
例如,控制文件上傳需要兩個(gè)節(jié)點(diǎn)按鈕,第一個(gè)用于暫停和繼續(xù)上傳,第二個(gè)用于刪除文件
- 當(dāng)文件在掃描狀態(tài)中,不能進(jìn)行任何操作,既不能暫停也不能刪除文件,只能等待掃描完成。掃描完成之后,根據(jù)文件的
md5值判斷,若確認(rèn)該文件已經(jīng)存在于服務(wù)器,則直接跳到上傳完成狀態(tài)。如果該文件的大小超過(guò)允許上傳的最大值,或者該文件已經(jīng)損壞,則跳往上傳失敗狀態(tài)。剩下的情況下才進(jìn)入上傳中狀態(tài) - 上傳過(guò)程中可以點(diǎn)擊暫停按鈕來(lái)暫停上傳,暫停后點(diǎn)擊同一個(gè)按鈕會(huì)繼續(xù)上傳
- 掃描和上傳過(guò)程中,點(diǎn)擊刪除按鈕無(wú)效,只有在暫停、上傳完成、上傳失敗之后,才能刪除文件
假設(shè)我們使用一個(gè)插件對(duì)象幫助我們完成上傳工作:
var plugin = (function () {
var plugin = document.createElement("embed");
plugin.style.display = "none";
plugin.type = "application/txftn-webkit";
plugin.sign = function () {
console.log("開(kāi)始文件掃描");
};
plugin.pause = function () {
console.log("暫停文件上傳");
};
plugin.uploading = function () {
console.log("開(kāi)始文件上傳");
};
plugin.del = function () {
console.log("刪除文件上傳");
};
plugin.done = function () {
console.log("文件上傳完成");
};
document.body.appendChild(plugin);
return plugin;
})();上傳是一個(gè)異步的過(guò)程,所以控件會(huì)不停地調(diào)用全局函數(shù)window.external.upload,來(lái)通知目前的上傳進(jìn)度,控件會(huì)把當(dāng)前的文件狀態(tài)作為參數(shù)state塞進(jìn)window.external.upload,在此例中該函數(shù)負(fù)責(zé)打印一些log:
window.external.upload = function (state) {
console.log(state); // 可能為 sign、uploading、done、error
};4.2 代碼過(guò)程
首先定義Upload類(lèi),在構(gòu)造函數(shù)中為每種狀態(tài)子類(lèi)都創(chuàng)建一個(gè)實(shí)例對(duì)象:
var Upload = function (fileName) {
this.plugin = plugin;
this.fileName = fileName;
this.button1 = null;
this.button2 = null;
this.signState = new SignState(this); // 設(shè)置初始狀態(tài)為 waiting
this.uploadingState = new UploadingState(); // 上傳中
this.pauseState = new PauseState(this); // 暫停
this.doneState = new DoneState(this); // 上傳完成
this.errorState = new ErrorState(this); // 上傳錯(cuò)誤
this.currState = this.signState; // 設(shè)置當(dāng)前狀態(tài)
};創(chuàng)建兩個(gè)按鈕,一個(gè)控制文件暫停和繼續(xù)上傳,一個(gè)用于刪除文件:
Upload.prototype.init = function () {
var that = this;
this.dom = document.createElement("div");
this.dom.innerHTML =
"<span>文件名稱(chēng):" +
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"]'); // 第一個(gè)按鈕
this.button2 = this.dom.querySelector('[data-action="button2"]'); // 第二個(gè)按鈕
this.bindEvent();
};為兩個(gè)按鈕分別綁定點(diǎn)擊事件,在點(diǎn)擊了按鈕之后,Context并不做任何具體的操作,而是把請(qǐng)求委托給當(dāng)前的狀態(tài)類(lèi)來(lái)執(zhí)行:
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 = "正在上傳,點(diǎn)擊暫停";
this.plugin.uploading();
this.currState = this.uploadingState;
};
// 暫停
Upload.prototype.pause = function () {
this.button1.innerHTML = "已暫停,點(diǎn)擊繼續(xù)上傳";
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);
};再接下來(lái)是編寫(xiě)各個(gè)狀態(tài)類(lèi)的實(shí)現(xiàn):
var StateFactory = (function () {
var State = function () {};
State.prototype.clickHandler1 = function () {
throw new Error("子類(lèi)必須重寫(xiě)父類(lèi)的 clickHandler1 方法");
};
State.prototype.clickHandler2 = function () {
throw new Error("子類(lèi)必須重寫(xiě)父類(lèi)的 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("掃描中,點(diǎn)擊無(wú)效...");
},
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("文件已完成上傳, 點(diǎn)擊無(wú)效");
},
clickHandler2: function () {
this.uploadObj.del();
},
});
var ErrorState = StateFactory({
clickHandler1: function () {
console.log("文件上傳失敗, 點(diǎn)擊無(wú)效");
},
clickHandler2: function () {
this.uploadObj.del();
},
});測(cè)試一下:
var uploadObj = new Upload("AAAAAAAAA");
uploadObj.init();
window.external.upload = function (state) {
// 插件調(diào)用 JavaScript 的方法
uploadObj[state]();
};
window.external.upload("sign"); // 文件開(kāi)始掃描
setTimeout(function () {
window.external.upload("uploading"); // 1 秒后開(kāi)始上傳
}, 1000);
setTimeout(function () {
window.external.upload("done"); // 5 秒后上傳完成
}, 5000);
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Javascript實(shí)現(xiàn)滾動(dòng)圖片新聞的實(shí)例代碼
這篇文章主要介紹了Javascript實(shí)現(xiàn)滾動(dòng)圖片新聞的實(shí)例代碼。需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-11-11
addEventListener()與removeEventListener()解析
這篇文章主要為大家詳細(xì)介紹了addEventListener()與removeEventListener(),用于處理指定和刪除事件處理程序操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
javascript中節(jié)點(diǎn)的最近的相關(guān)節(jié)點(diǎn)訪問(wèn)方法
parentNode——父節(jié)點(diǎn);firstChild——第一個(gè)子節(jié)點(diǎn);lastChild——最后一個(gè)子節(jié)點(diǎn);previousSibling——緊挨著的前面的兄弟節(jié)點(diǎn);這樣就可以作短途旅行,訪問(wèn)當(dāng)前節(jié)點(diǎn)的某些相關(guān)節(jié)點(diǎn),感興趣的你可以參考下哈2013-03-03
javascript實(shí)現(xiàn)校驗(yàn)文件上傳控件實(shí)例
這篇文章主要介紹了javascript實(shí)現(xiàn)校驗(yàn)文件上傳控件,實(shí)例分析了javascript檢測(cè)上傳文件類(lèi)型是否為圖片的功能,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04
前端try?catch的核心目的與應(yīng)用場(chǎng)景示例詳解
在Vue中處理異常的一種簡(jiǎn)單方法是使用JavaScript中的try-catch語(yǔ)句,這篇文章主要介紹了前端try?catch的核心目的與應(yīng)用場(chǎng)景的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2026-02-02

