來(lái)寫(xiě)一個(gè)簡(jiǎn)單的計(jì)算器吧!(JS)

每個(gè)簡(jiǎn)單易用的設(shè)計(jì)背后,必定有縝密細(xì)心的邏輯。
這是我在試著寫(xiě)完這個(gè)自認(rèn)為大概應(yīng)該再?zèng)]有什么bug的計(jì)算器后最大的感嘆。從未想過(guò)只有加減乘除,還不帶括號(hào)的簡(jiǎn)單計(jì)算器,具體寫(xiě)起來(lái)還真不簡(jiǎn)單。
一開(kāi)始思路就沒(méi)對(duì),只是想到哪寫(xiě)哪,然而越寫(xiě)就邏輯越復(fù)雜就越亂,最后決定重新思考整個(gè)構(gòu)建模式,以下是個(gè)人覺(jué)得應(yīng)該對(duì)的思路。
從需求出發(fā)
應(yīng)該能保留輸入歷史,一行一行顯示
按等號(hào)就換行,計(jì)算輸出
輸錯(cuò)能退格,能全部清除
結(jié)果能作為輸入繼續(xù)運(yùn)算
具體計(jì)算有兩種思路:
1.用eval函數(shù)直接傳入表達(dá)式計(jì)算
2.用逆波蘭表達(dá)式和棧,分析出輸入的值來(lái)計(jì)算
從偷懶的角度考慮,還是果斷選前者了!
開(kāi)始
關(guān)鍵思維是這樣的:
從整體上先對(duì)計(jì)算器的狀態(tài)進(jìn)行劃分:初始輸入狀態(tài),正常輸入狀態(tài),結(jié)果狀態(tài)
分別記作0,1,2,通過(guò)狀態(tài)來(lái)控制輸入
0狀態(tài):準(zhǔn)備首次輸入,或是全部清零后準(zhǔn)備輸入
1狀態(tài):在首次輸入之后準(zhǔn)備繼續(xù)輸入(還沒(méi)按等號(hào)),或是按等號(hào)以后,使用結(jié)果繼續(xù)準(zhǔn)備計(jì)算
2狀態(tài):按下等號(hào)以后
為什么要?jiǎng)澐譅顟B(tài)?
換行的需求導(dǎo)致。0狀態(tài),需要新建一行來(lái)準(zhǔn)備輸入用,2狀態(tài),在不需要接著結(jié)果算的時(shí)候也要新建一行。
一行使用一個(gè)<p>元素,用id區(qū)分,id每行加一,從0開(kāi)始。新建一行這個(gè)操作就可以寫(xiě)成一個(gè)函數(shù)以備后用
let newLine = function() {
count++;
let newLine = document.createElement("p");
newLine.setAttribute("id", count);
results.appendChild(newLine);
}
其中count記錄了當(dāng)前行,count即是id,初始化count = -1
狀態(tài)使用flag記錄,初始化flag = 0
對(duì)于輸入類(lèi)型,也進(jìn)行了以下劃分,共七種
等號(hào)輸入
點(diǎn)號(hào)輸入
非零數(shù)字輸入
零輸入
運(yùn)算符輸入
回退輸入
清零輸入
其中對(duì)于數(shù)字,為什么要?jiǎng)澐殖?,還是因?yàn)槠涮厥庑?/p>
零會(huì)對(duì)后續(xù)輸入產(chǎn)生影響,而其余的輸入只是依據(jù)先前狀態(tài)決定自身行為
舉例:
首次輸入時(shí),按下0,顯然其后不應(yīng)跟數(shù)字,應(yīng)該是點(diǎn)號(hào),意指輸入一個(gè)小數(shù)
這樣,零其實(shí)和狀態(tài)flag相似,因此在狀態(tài)控制上再加一維,初始化zeroSwitch = 0
接著例子,輸入零后,使zeroSwitch = 1,zeroSwitch用0和1來(lái)控制,相當(dāng)于是一個(gè)篩子,之后只能輸點(diǎn)號(hào),或清除
所以全部的狀態(tài)有三種:count,flag,zeroSwitch
接著來(lái)看輸入
輸入一共有七種類(lèi)型,分別對(duì)應(yīng)一個(gè)函數(shù)
對(duì)于零的輸入,zero函數(shù):
let zero = function() {
if (zeroSwitch != 1) { //強(qiáng)制在zeroSwitch=1時(shí),其后只能輸入點(diǎn)號(hào)
if (flag != 1) { //0,2狀態(tài),新建一行
newLine();
flag = 1; //變成正常行
zeroSwitch = 1; //新建一行后輸入零,zeroSwitch需要置1,因?yàn)樵摿闶且粋€(gè)數(shù)字之始
document.getElementById(count).innerHTML += event.target.value; //插入輸入的值
} else {
let theString = document.getElementById(count).innerHTML,
lastWord = theString[theString.length - 1],
Ope = RegExp(/[-x÷\+]/);
if (Ope.test(lastWord)) { //匹配已經(jīng)輸入的最后一個(gè)字符,如果是運(yùn)算符,則這個(gè)零是一個(gè)數(shù)字之始,后也只跟點(diǎn)號(hào)
zeroSwitch = 1;
document.getElementById(count).innerHTML += event.target.value;
} else { //非數(shù)字之始,沒(méi)限制
document.getElementById(count).innerHTML += event.target.value;
}
}
}
}
對(duì)于點(diǎn)號(hào)輸入,dot函數(shù):
dot = function() {
if (flag != 1) {
newLine();
flag = 1;
zeroSwitch = 0; //零輸入后,只能跟點(diǎn)號(hào)的狀態(tài)關(guān)閉
document.getElementById(count).innerHTML += event.target.value;
} else {
let theString = document.getElementById(count).innerHTML,
i = theString.length - 1;
Ope = RegExp(/[-x÷\+]/);
dots = RegExp(/\./);
for (i; i >= 0; i--) { //從后往前檢查已輸入值,一個(gè)數(shù)字內(nèi)是不會(huì)有兩個(gè)點(diǎn)號(hào)的
if (dots.test(theString[i])) { //遇到一個(gè)數(shù)字內(nèi)已有點(diǎn)號(hào),就不輸入了
break;
} else if (Ope.test(theString[i])) { //沒(méi)有點(diǎn)號(hào),先找到了一個(gè)運(yùn)算符,就可以輸入
zeroSwitch = 0;
document.getElementById(count).innerHTML += event.target.value;
break;
}
}
if (i < 0) { //全部檢查完了,既沒(méi)點(diǎn)號(hào)又沒(méi)運(yùn)算符,那就輸入唄
zeroSwitch = 0;
document.getElementById(count).innerHTML += event.target.value;
}
}
}
對(duì)于等號(hào)輸入,equal函數(shù):
equal = function() {
if (flag != 0) {
let buffer; //因?yàn)橛?jì)劃的輸入是數(shù)學(xué)符號(hào),乘和除,運(yùn)算時(shí)要轉(zhuǎn)換
buffer = document.getElementById(count).innerHTML.replace(/x/g, "*");
buffer = buffer.replace(/÷/g, "/");
try { //萬(wàn)一式子不對(duì)呢
if (new Function("return " + buffer)()) { //構(gòu)造函數(shù)算值,實(shí)際就是eval函數(shù)
newLine();
flag = 2; //轉(zhuǎn)為結(jié)果行狀態(tài)
document.getElementById(count).innerHTML = new Function("return " + buffer)();
}
} catch (e) {}
}
}
對(duì)于數(shù)字輸入,num函數(shù):
num = function() {
if (zeroSwitch != 1) { //就是零輸入的大篩子
if (flag != 1) {
newLine();
flag = 1;
document.getElementById(count).innerHTML += event.target.value;
} else {
document.getElementById(count).innerHTML += event.target.value;
}
}
}
對(duì)于運(yùn)算符輸入,ope函數(shù):
ope = function() {
if (zeroSwitch != 1) {
let validOpe = RegExp(/[-\+]/),
reValidOpe = RegExp(/[x÷]/),
Ope = RegExp(/[-x÷\+]/);
if (flag == 0) {
if (validOpe.test(event.target.value)) { //允許正負(fù)號(hào)開(kāi)頭
newLine();
flag = 1;
document.getElementById(count).innerHTML += event.target.value;
}
} else if (flag == 1) {
let theString = document.getElementById(count).innerHTML,
lastWord = theString[theString.length - 1];
if (reValidOpe.test(lastWord) && event.target.value == "-") {//禁止符號(hào)重復(fù)輸入,但可以輸入負(fù)號(hào),表示負(fù)數(shù)
document.getElementById(count).innerHTML += event.target.value;
} else if (!Ope.test(lastWord)) { //前面沒(méi)符號(hào)就接著輸入唄
document.getElementById(count).innerHTML += event.target.value;
}
} else { //結(jié)果行,連算
flag = 1;
document.getElementById(count).innerHTML += event.target.value;
}
}
}
對(duì)于回退,back函數(shù):
back = function() {
let theString = document.getElementById(count).innerHTML;
if (flag == 2) { //結(jié)果行回退,自然轉(zhuǎn)換為正常輸入了
document.getElementById(count).innerHTML = theString.slice(0, theString.length - 1);
flag = 1;
zeroSwitch = 0;
} else if (flag == 1) {
document.getElementById(count).innerHTML = theString.slice(0, theString.length - 1);
zeroSwitch = 0;
}
}
對(duì)于清零,allClear函數(shù):
allClear = function() {
if (flag != 0) {
let allNodes = results.childNodes;
for (let i = allNodes.length - 1; i >= 0; i--) {//從后往前刪,方便
results.removeChild(allNodes.item(i));
}
flag = 0;
count = -1;
zeroSwitch = 0;
}
}
最后的完工
operator.addEventListener("click", //操作面板,一整塊區(qū)域
function functionName(event) {//事件委托一下
if (event.target && event.target.nodeName.toLowerCase() == "button") {
clicked = event.target.className;
eval(clicked + "()");//拼湊出調(diào)用的函數(shù)
results.scrollTo(0, results.offsetHeight);//這個(gè)么,在你輸入過(guò)多東西的時(shí)候,滾動(dòng)條置底
}
});
這里是鏈接
https://codepen.io/mxxxxxs/full/vpQjJQ/
這是總的思路圖,當(dāng)時(shí)碼字的藍(lán)圖

這就是全部了,分享一下以供參考!(?ˉ?ˉ?)