最近想用js做一個簡單的計算器,不過網上的例子好像大部分都是直接從左到右挨個計算,就好像1+2*5,就會先計算1+2,再計算3*5,並沒有實現運算符的優先級,這里找到了一種方法實現,來總結一下。不過這里只是最基本的思路,還有許許多多的細節沒有完善。
在解決基本的樣式布局與交互邏輯之前,我們先來解決四則混合運算的核心模塊,也就是如何把我們輸入的字符串轉換為數字表達式並進行計算:
我所找到的方法叫做逆波蘭表達式(也叫做后綴表達式),關於逆波蘭表達式的具體定義大家可以上網去搜索一下,概念應該比較簡單。
這里我們舉一個例子來展示一下逆波蘭表達式的作用:
例如:3+4*5 ,這個表達式要如何實現先算乘號再算加號呢?對於計算機來說應該很難實現
但是把它轉換成這個式子再看一下:
3,4,5,*,+,那么這樣看起來好像就簡單多了,只要每遇到一個操作符就將他的前兩個操作數進行運算,再將操作結果代替運算表達式直到算出最終結果,這樣說有點復雜,我們還是看一下例子
3,4,5,*,+ -> 3,20,+ -> 23
那么該如何把我們熟悉的 3+4*5 轉變成這個牛逼的逆波蘭表達式呢?
大家也可以上網搜索一下,這里我給出一個我找到的鏈接:
為了方便大家順暢的瀏覽文章,這里截取文章的部分核心內容(即轉換規則):
一般算法:
逆波蘭表達式的一般解析算法是建立在簡單算術表達式上的,它是我們進行公式解析和執行的基礎:
1. 構建兩個棧Operand(操作數棧)和Operator(操作符棧)。
2.掃描給定的字符串,如果得到一個數字,則提取(掃描是一位一位的,一定要提取一個完整的數字)數字(以下用Operand代替),然后把Operand壓入Operand棧中。
3. 如果獲得一個運算符(比如+或者*,下面用B代替),則需要和Operator棧棧頂元素(用A替代)比較:
1) 如果A不存在,則把B壓入Operator棧中;
2)如果B是一個左括號,則忽略A和B的優先級比較,把B壓入Operator棧。
3)如果B是一個右括號,則把Operator棧順序出棧,然后把彈出的元素順序壓入Operand棧中,直到棧頂彈出的是左括號,括號不入Operand棧中。
4)如果A是左括號,則把B直接壓入Operator棧。
5)如果B優先級比較A高,則把B直接壓入Operator棧。
6)如果B優先級低於或等於A的優先級,則把A出棧然后壓入Operand棧,反復進行此步驟直到棧頂優先級高於B的優先級或者棧頂是一個括號。
4.掃描完畢后,把Operator棧的元素依次出棧,然后依次壓入Operand棧中。
雖然不太 明白原理,不過跟着一步步做就可以得到逆波蘭表達式了。這里一般會在一開始往operator里面壓入一個“#”,並把它的優先級設置為最低,這樣就方便其他運算符來進行比較了。
現在我們來理一下思路:為了得到逆波蘭表達式我們需要以下幾個步驟:
1.將字符串轉換為數組,轉化過程中要將操作數和操作符分開,直接操作字符串的話,會出現錯誤,例如:
3+20 會被解析成: 3,+,2,0
2.在數組前加一個“#”,方便進行操作符的比較。
3.根據上面給出的規則進行編碼得到operant數組
首先是字符串的轉換:
這里是我想出來一種比較笨的方法,就是在操作符兩邊都加上一個分隔符,在根據這個分隔符來進行分割。應該有更簡單的方法,大家可以在評論區討論下。
var operand = [], //用於存放操作數的棧 operator = [], //用於存放操作符的棧 textArr = text.split(''), newTextArr = [], calTextArr = []; //用於存放操作數與操作符分割后的數組。 for(var i = 0; i < textArr.length; i++){ if(!Number(text[i])){ newTextArr.push("|",text[i],"|"); } else{ newTextArr.push(textArr[i]); } } calTextArr = newTextArr.join('').split("|"); calTextArr.unshift("#")
然后就是根據規則一步步來了,但是其中有一個運算符的優先級比較我們還沒有解決
運算符的優先級比較
這里我們把每一個運算符的優先級都用數字來表示就更加清晰明了。
/* *比較操作符的優先級 *param string 需要被轉換的字符串 */ function compareOperator(a,b){ var aLevel = getOperatorRand(a), bLevel = getOperatorRand(b); if(aLevel <= bLevel){ return true; } else if(aLevel > bLevel){ return false; } } /* *將操作符的優先級用數字具體化 */ function getOperatorRand(operator){ switch(operator){ case "#": return 0; case "+": return 1; break; case "-": return 1; break; case "*": return 2; break; case "/": return 2; break; } }
運算符的優先級比較問題也已經解決了,然后我們就可以得到逆波蘭表達式了:
得到逆波蘭表達式(其中text是待轉換的字符串):
function getRPN(text){ var operand = [], //用於存放操作數的棧 operator = [], //用於存放操作符的棧 textArr = text.split(''), newTextArr = [];
for(var i = 0; i < textArr.length; i++){ if(!Number(text[i]) && Number(text[i]) != 0){ newTextArr.push("|",text[i],"|"); } else{ newTextArr.push(textArr[i]); } } var calTextArr = newTextArr.join('').split("|"); calTextArr.unshift("#") for(var i = 0; i < calTextArr.length; i++){ //如果是數字則直接入棧 if(Number(calTextArr[i]) || Number(calTextArr[i]) == 0){ operand.push(calTextArr[i]); } //如果是操作符則再根據不同的情況進行操作 else { switch(true){ //如果operator棧頂是“(”或者遍歷到的操作符是“(”則直接入棧 case calTextArr[i] == "(" && operator.slice(-1)[0] == "(": operator.push(calTextArr[i]); break; /*如果遍歷到的操作符是“)”則把operator中的操作符依次彈出並壓入 operand中直至operator棧頂操作符為“(”,然后將“(”也彈出,但不壓入 operand棧中 */ case calTextArr[i] == ")": do{ operator.push(operator.pop()); }while(operator.slice(-1)[0] != "("); operator.pop(); break; //如果是其他的操作符,則比較優先級后再進行操作 default: var compare = compareOperator(calTextArr[i],operator.slice(-1)[0]); var a = calTextArr[i]; var b = operator.slice(-1)[0] if(operator.length == 0){ operator.push(calTextArr[i]); } else if(compareOperator(calTextArr[i],operator.slice(-1)[0])){ do{ operand.push(operator.pop()); var compareResult = compareOperator(calTextArr[i],operator.slice(-1)[0]); }while(compareResult); operator.push(calTextArr[i]); } else { operator.push(calTextArr[i]); } break; } } } //遍歷結束后,將operator中的元素全部壓入operand中 operator.forEach(function(){ operand.push(operator.pop()); }); //把用於比較的“#”字符去掉 operator.pop(); return operand; }
ok,逆波蘭表達式我們已經搞定了,接下來就可以計算了,計算的思路一開始也講過了,就是每遇到一個操作符就將他的前兩個操作數進行運算,再將操作結果代替運算表達式,這樣循環下去直到算出最終結果,不過這里我的代碼顯得有點多而且麻煩,本人水平有限,有更好的方法請在評論區指出。
/* *計算並返回結果 */ function getResult(RPNarr){ var result; while(RPNarr.length > 1) RPNarr = singleResult(RPNarr); console.log(RPNarr) result = RPNarr[0]; return result; } /* *每遇到一個操作符就進行一次運算然后更新數組,直到算出最終結果。 */ function singleResult(RPNarr){ for(var i = 0,max = RPNarr.length; i < max; i++){ console.log(!Number(RPNarr)) if(!Number(RPNarr[i])){ switch(RPNarr[i]){ case "+": var addResult = Number(RPNarr[i-2]) + Number(RPNarr[i-1]); RPNarr.splice(i-2,3,addResult); return RPNarr; break; case "-": var addResult = Number(RPNarr[i-2]) - Number(RPNarr[i-1]); RPNarr.splice(i-2,3,addResult); return RPNarr; break; case "*": var addResult = Number(RPNarr[i-2]) * Number(RPNarr[i-1]); RPNarr.splice(i-2,3,addResult) return RPNarr; break; case "/": var addResult = Number(RPNarr[i-2]) / Number(RPNarr[i-1]); RPNarr.splice(i-2,3,addResult) return RPNarr; break; } } } }
至此,我們的運算過程就全部結束了。
然后就是頁面的布局,布局和樣式大家可以隨意實現這里就簡單貼下代碼:
基本布局與樣式:
html代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> </head> <body> <div id="calculator-container"> <h1>Bett's Calculator</h1> <div class="calculator-display"> <input id="calculator-display" type="text"> </div> <ul class="btn-list"> <li class="btn-item">1</li> <li class="btn-item">2</li> <li class="btn-item">3</li> <li class="btn-item">+</li> <li class="btn-item">4</li> <li class="btn-item">5</li> <li class="btn-item">6</li> <li class="btn-item">-</li> <li class="btn-item">7</li> <li class="btn-item">8</li> <li class="btn-item">9</li> <li class="btn-item">*</li> <li class="btn-item">.</li> <li class="btn-item">0</li> <li class="btn-item" id="result">=</li> <li class="btn-item">/</li> </ul> </div> <script src="calculator.js"></script> </body> </html>
css代碼:
html,body,button,h1,h2,div,p,input,ul,li { margin: 0; padding: 0; } ul,li{ list-style: none; } h1 { text-align: center; color: #fff; margin-bottom: 20px; font-weight: normal; } #calculator-container { width: 300px; height: auto; margin: 100px auto 0; padding: 20px 20px; background-color: #354B69; overflow: hidden; } .calculator-display { width: 100%; margin-bottom: 10px; } #calculator-display { display: block; width: 100%; padding: 5px 10px 5px 0; text-align: right; font-size: 18px; line-height: 30px; border: none; box-sizing: border-box; } .btn-item { float: left; width: 24%; padding: 6% 0; margin: 0.5%; text-align: center; font-size: 18px; font-weight: bold; color: #354B69; background-color: #fff; }
最后的效果圖是長這樣:
有許多功能還沒有實現,比如清空,退格,括號的運算等,都還沒加進去,有一些小bug可能自己也沒發現,這里主要講解一下思路,大家后面可以自己拓展,我做出完整功能后也會來更新。
基本的交互邏輯的思路:
/**
1、首先我需要將我點擊的任意一個按鈕(除了等號按鈕外)的文本顯示在input框中
2、在按下等號時將input框中的文本拿出來
3、將得到的字符串轉換成數學表達式並進行計算
4、將計算結果反應在input框中
5、計算完成后若再次點擊的是數字則清空顯示框再進行下一次運算,如果點擊的是操作符則繼續進行運算
*/
那么這里首先就有一個問題,就是關於計算狀態的判斷,根據上面的思路我們可以有這樣一個思路(input框的value值用val表示):
a. 如果我點擊的是普通按鈕,那么顯示框中就用val+=不斷追加並更新內容,我們把這個狀態稱為“continue”(計算中)
b. 如果我點擊的是等號按鈕,那么就算出結果result,然后清空搜索框再更新結果,val = result 我們把這個狀態稱為“end”(計算結束)
但是因為有了第5步,我們的狀態判斷變得更為復雜了一些:
在按下等號之后:又會出現兩種狀態:
a. 如果我點擊的是操作符,那么就繼續進行運算,狀態更新為“continue”
b. 如果我點擊的是數字,那么就重新開始運算,這里我們給一個新狀態“start”(“重新開始運算”)
那我們怎么去判斷等號后的下一個按鈕是什么呢?感覺很難判斷,那我們干脆就在點擊普通按鈕的時候都來進行一個狀態的判斷,根據得到的狀態來決定如何在顯示框中進行顯示。這里給出代碼:
設置狀態
首先我們設置一個全局變量“state”,默認狀態為“start”
var calState = "start";
狀態判斷(其中參數text是點擊的按鈕的文本內容)
/* *點擊普通按鈕時進行狀態判斷 *如果是“continue”狀態則繼續運算 *如果是“end”狀態,則再根據操作的不同進行判斷 */ function setCalState(text){ if(calState == 'end' && Number(text) || (calState == 'end' && Number(text) == 0)){ calState = 'start'; } else if(calState == 'end' && !Number(text)){ calState = 'continue'; } else { calState = 'continue'; } }
根據狀態的不同顯示框中的顯示也有不同的方式:
function setInputValue(text){ var calInput = document.getElementById('calculator-display'); if(calState == "end" || calState == "start"){ calInput.value = text } else{ calInput.value += text; } }
利用事件冒泡原理給每一個li 綁定一個點擊事件
/* *利用事件冒泡給每一個按鈕添加點擊事件 */ function btnHandleClick(callback){ var btnList = document.getElementsByClassName('btn-list')[0]; btnList.onclick = function(e){ var btnEl = e.target || window.e.target; if(btnEl.id == 'result'){ calState = 'end';
var resultText = getInputValue(); var RPNarr = getRPN(resultText); var totalResult = getResult(RPNarr); callback(''); callback(totalResult); } else{ var btnText = btnEl.innerText; setCalState(btnText); callback(btnText); } } } btnHandleClick(setInputValue);
至此我們就用js完成了一個簡單的四則混合運算的計算器,不過還有一些缺陷,比如說js在進行加減乘除時會有一些精度的問題,比如0.1+0.2 != 0.3,而是等於
0.30000000000000004,類似這樣的精度問題,其實可以把getResult中每個操作符的運算抽離出來,例如加法的運算可以單獨拿出來做一些處理
寫成function add(){} ,然后再進行一些精度的處理。
一種更加簡單但不推薦的方法
其實還有一種簡單的方法,就是eval(text) ,輸入字符串后字符串中的語句就會被自動執行,一行代碼就搞定,相當方便,可是高程上並不推薦這種做法
說是不太安全,萬一人家輸入什么亂七八糟的字符串也會被執行,另外一種 new Function(str)的方法相對安全點,但也是類似的思路
而且最重要的是如果運用了這種方法的話,我們就無法自己對運算過程進行操作了,比如說上面的加法運算的精度問題,還有其他的一些問題,所以我們還
是采用自己實現混合運算的方法。
非常重要的一點:上面的代碼只是一個思路,有許多細節部分都沒有處理,比如除數不能為0等地方的錯誤處理也沒有。希望大家看的時候能注意下。
本人是一只小白,上述部分如有錯漏之處,或者說有更好的思路、方法請在評論區指出