js實現四則混合運算計算器


最近想用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等地方的錯誤處理也沒有。希望大家看的時候能注意下。
本人是一只小白,上述部分如有錯漏之處,或者說有更好的思路、方法請在評論區指出


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM