JS實現簡易的計算器


JS可以做的事多了,那就用來實現一個計算器吧

 

看看手機中的計算器,分為普通計算器和科學計算器

   

 

自認腦袋不夠大,就實現一個普通版本的吧(支持正負數加減乘除等基本連續的運算,未提供括號功能)

看看圖示效果, 或 在線演示

 

一、知識准備

1+1 = ?

正常來說,我們看到這個表達式都知道怎么運算,知道運算結果

但計算機不一樣,計算機無法識別出這串表達式,它只能識別特定的規則:前綴表達式+ 1 1 或后綴表達式1 1 +

舉個栗子

(3 + 4) × 5 - 6 就是中綴表達式
- × + 3 4 5 6 前綴表達式
3 4 + 5 × 6 - 后綴表達式

 

所以為了實現程序的自動運算,我們需要將輸入的數據轉化為前綴或后綴表達式

前綴、中綴、后綴表達式的概念以及相互轉換方法在這里就不多說了,這篇博文 說得比較清楚了 

所以,在這個計算器的實現中,采用了后綴表達式的實現方式,參考以上文章,重點關注這兩個算法:

與轉換為前綴表達式相似,遵循以下步驟:
(1) 初始化兩個棧:運算符棧S1和儲存中間結果的棧S2;
(2) 從左至右掃描中綴表達式;
(3) 遇到操作數時,將其壓入S2;
(4) 遇到運算符時,比較其與S1棧頂運算符的優先級:
(4-1) 如果S1為空,或棧頂運算符為左括號“(”,則直接將此運算符入棧;
(4-2) 否則,若優先級比棧頂運算符的高,也將運算符壓入S1(注意轉換為前綴表達式時是優先級較高或相同,而這里則不包括相同的情況);
(4-3) 否則,將S1棧頂的運算符彈出並壓入到S2中,再次轉到(4-1)與S1中新的棧頂運算符相比較;
(5) 遇到括號時:
(5-1) 如果是左括號“(”,則直接壓入S1;
(5-2) 如果是右括號“)”,則依次彈出S1棧頂的運算符,並壓入S2,直到遇到左括號為止,此時將這一對括號丟棄;
(6) 重復步驟(2)至(5),直到表達式的最右邊;
(7) 將S1中剩余的運算符依次彈出並壓入S2;
(8) 依次彈出S2中的元素並輸出,結果的逆序即為中綴表達式對應的后綴表達式(轉換為前綴表達式時不用逆序)。
將中綴表達式轉換為后綴表達式:
與前綴表達式類似,只是順序是從左至右:
從左至右掃描表達式,遇到數字時,將數字壓入堆棧,遇到運算符時,彈出棧頂的兩個數,用運算符對它們做相應的計算(次頂元素 op 棧頂元素),並將結果入棧;重復上述過程直到表達式最右端,最后運算得出的值即為表達式的結果。
例如后綴表達式“3 4 + 5 × 6 -”:
(1) 從左至右掃描,將3和4壓入堆棧;
(2) 遇到+運算符,因此彈出4和3(4為棧頂元素,3為次頂元素,注意與前綴表達式做比較),計算出3+4的值,得7,再將7入棧;
(3) 將5入棧;
(4) 接下來是×運算符,因此彈出5和7,計算出7×5=35,將35入棧;
(5) 將6入棧;
(6) 最后是-運算符,計算出35-6的值,即29,由此得出最終結果。
后綴表達式的計算機求值:

 

二、實現過程

第一步當然是搭建計算器的頁面結構,不是科學計算器,只提供了基本的運算功能,但也能即時地進行運算,顯示出完整的中綴表達式,運算后保存上一條運算記錄。

要先說一下:本來想實現小數點功能的,但小數點的存在讓數據存儲與數據顯示的實現有了壓力,實現過程實在腦大,索性先取消這個功能。

 

1. 頁面結構:

    <h5>計算計算</h5>
    <!-- 計算器 -->
    <div class="calc-wrap">
        <div class="calc-in-out">
            <!-- 上一條運算記錄 -->
            <p class="calc-history" title=""></p>
            <!-- 輸入的數據 -->
            <p class="calc-in"></p>
            <!-- 輸出的運算結果 -->
            <p class="calc-out active"></p>
        </div>
        <table class="calc-operation">
            <thead></thead>
            <tbody>
                <tr>
                    <td data-ac="cls" class="cls">C</td>
                    <td data-ac="del">&larr;</td>
                    <td data-ac="sq">x<sup>2</sup></td>
                    <td data-ac="mul">&times;</td>
                </tr>
                <tr>
                    <td data-val="7">7</td>
                    <td data-val="8">8</td>
                    <td data-val="9">9</td>
                    <td data-ac="div">&divide;</td>
                </tr>
                <tr>
                    <td data-val="4">4</td>
                    <td data-val="5">5</td>
                    <td data-val="6">6</td>
                    <td data-ac="plus">+</td>
                </tr>
                <tr>
                    <td data-val="1">1</td>
                    <td data-val="2">2</td>
                    <td data-val="3">3</td>
                    <td data-ac="minus">-</td>
                </tr>
                    <td data-ac="per">%</td>
                    <td data-val="0">0</td>
                    <td data-ac="dot">.</td>
                    <td data-ac="eq" class="eq">=</td>
            </tbody>
        </table>
    </div>

2. 結合一點樣式:

body {
    padding: 20px;
    font-family: Arial;
}

.calc-wrap {
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 3px;
}


.calc-operation {
    width: 100%;
    border-collapse: collapse;
}

.calc-in-out {
    width: 100%;
    padding: 10px 20px;
    text-align: right;
    box-sizing: border-box;
    background-color: rgba(250, 250, 250, .9);
}
.calc-in-out p {
    overflow: hidden;
    margin: 5px;
    width: 100%;
}
.calc-history {
    margin-left: -20px;
    font-size: 18px;
    color: #bbb;
    border-bottom: 1px dotted #ddf;
    min-height: 23px;
}

.calc-in,
.calc-out {
    font-size: 20px;
    color: #888;
    line-height: 39px;
    min-height: 39px;
}

.calc-in {
    color: #888;
}
.calc-out {
    color: #ccc;
}

.calc-in.active,
.calc-out.active {
    font-size: 34px;
    color: #666;
}

.calc-operation td {
    padding: 10px;
    width: 25%;
    text-align: center;
    border: 1px solid #ddd;
    font-size: 26px;
    color: #888;
    cursor: pointer;
}

.calc-operation td:active {
    background-color: #ddd;
}

.calc-operation .cls {
    color: #ee8956;
}
CSS樣式

這樣靜態的計算器就粗來了~~

 

3. JS邏輯

這部分就是重點了,一步步來說

首先是對計算器的監聽吧,也就是這個表格,可以使用事件委托的方式,在父級節點上監聽處理

        // 綁定事件
        bindEvent: function() {
            var that = this;

            that.$operation.on('click', function(e) {
                e = e || window.event;
                var elem = e.target || e.srcElement,
                    val,
                    action;

                if (elem.tagName === 'TD') {
                    val = elem.getAttribute('data-val') || elem.getAttribute('data-ac');    

            ...

監聽數據,獲取到的只是頁面上的某個值/操作符,所以需要將數據存儲起來形成中綴,再由中綴轉換成后綴,最后通過后綴進行計算

        // 中綴表達式
        this.infix = [];
        // 后綴表達式
        this.suffix = [];
        // 后綴表達式運算結果集
        this.result = [];

按照算法步驟,實現出來,這里沒有使用到括號,如果實際需要,可在相應位置修改判斷條件即可~

        // 中綴表達式轉后綴
        infix2Suffix: function() {
            var temp = [];
            this.suffix = [];

            for (var i = 0; i < this.infix.length; i++) {
                // 數值,直接壓入
                if (!this.isOp(this.infix[i])) {
                    this.suffix.push(this.infix[i]);
                }
                else {
                    if (!temp.length) {
                        temp.push(this.infix[i]);
                    }
                    else {
                        var opTop = temp[temp.length - 1];
                        // 循環判斷運算符優先級,將運算符較高的壓入后綴表達式
                        if (!this.priorHigher(opTop, this.infix[i])) {
                            while (temp.length && !this.priorHigher(opTop, this.infix[i])) {
                                this.suffix.push(temp.pop());
                                opTop = temp[temp.length - 1];
                            }
                        }
                           // 將當前運算符也壓入后綴表達式
                        temp.push(this.infix[i]);
                    }
                }
            }
            // 將剩余運算符號壓入
            while (temp.length) {
                this.suffix.push(temp.pop());
            }
        },
        // 后綴表達式計算
        calcSuffix: function() {
            this.result = [];

            for (var i = 0; i < this.suffix.length; i++) {
                // 數值,直接壓入結果集
                if (!this.isOp(this.suffix[i])) {
                    this.result.push(this.suffix[i]);
                }
                // 運算符,從結果集中取出兩項進行運算,並將運算結果置入結果集合
                else {
                    this.result.push(this.opCalc(this.result.pop(), this.suffix[i], this.result.pop()));
                }
            }
            // 此時結果集中只有一個值,即為結果
              return this.result[0];
        }

其實,在實現的時候會發現,中綴、后綴只是一個難點,更復雜的地方是整個計算器的狀態變化(或者說是數據變化)

在這個簡單的計算器中,就有數字(0-9)、運算符(+ - * /)、操作(清除 刪除)、預運算(百分號 平方)、小數點、即時運算等數據及操作

如果是科學計算器那就更復雜了,所以理清如何控制這些東西很關鍵,而其中最重要的就是中綴表達式的構建與存儲

 

當連續點擊+號時,是不符合實際操作的,所以需要一個變量 lastVal 來記錄上一個值,隨着操作而更新,再通過判斷,防止程序出錯

在點擊=號之后,我們可以繼續使用這個結果進行運算,或者重新開始運算

    // 構建中綴表達式
        buildInfix: function(val, type) {
            // 直接的點擊等於運算之后,
            if (this.calcDone) {
                this.calcDone = false;
                // 再點擊數字,則進行新的運算
                if (!this.isOp(val)) {
                    this.resetData();
                }
                // 再點擊運算符,則使用當前的結果值繼續進行運算
                else {
                    var re = this.result[0];
                    this.resetData();
                    this.infix.push(re);
                }

            }

            var newVal;
             ...

點擊刪除,是刪除一位數,不是直接地刪除一個數,然后更新中綴表達式的值

            // 刪除操作
            if (type === 'del') {
                newVal = this.infix.pop();
                // 刪除末尾一位數
                newVal = Math.floor(newVal / 10);
                if (newVal) {
                    this.infix.push(newVal);
                }

                this.lastVal = this.infix[this.infix.length - 1];
                return this.infix;
            }    

而添加操作,要考慮的就更多了,比如連續的連續運算符、連續的數字、運算符+ - 接上數字表示正負數,小數點的連接存取等

            // 添加操作,首先得判斷運算符是否重復
            else if (type === 'add') {
                // 兩個連續的運算符
                if (this.isOp(val) && this.isOp(this.lastVal)) {
                    return this.infix;
                }
                // 兩個連續的數字
                else if (!this.isOp(val) && !this.isOp(this.lastVal)) {
                    newVal = this.lastVal * 10 + val;
                    this.infix.pop();
                    this.infix.push(this.lastVal = newVal);

                    return this.infix;
                }
                // 首個數字正負數
                if (!this.isOp(val) && this.infix.length === 1 && (this.lastVal === '+' || this.lastVal === '-')) {
                    newVal = this.lastVal === '+' ? val : 0 - val;
                    this.infix.pop();
                    this.infix.push(this.lastVal = newVal);

                    return this.infix;
                }


                this.infix.push(this.lastVal = val);
                return this.infix;
            }

在很多次操作的時候,計算器都需要即時地進行運算,為簡化代碼,可以封裝成一個方法,在相應的位置調用即可

        // 即時得進行運算
        calculate: function(type) {
            this.infix2Suffix();
            var suffixRe = this.calcSuffix();

            if (suffixRe) {
                this.$out.text('=' + suffixRe)
                    .attr('title', suffixRe)
                    .removeClass('active');

                // 如果是直接顯示地進行等於運算
                if (type === 'eq') {
                    this.$in.removeClass('active');
                    this.$out.addClass('active');
                    // 設置標記:當前已經顯示地進行計算
                    this.calcDone = true;
                    this.lastVal = suffixRe;
                    // 設置歷史記錄
                    var history = this.infix.join('') + ' = ' + suffixRe;
                    this.$history.text(history).attr('title', history);
                }

            }
        },

剩下的就是點擊之后的處理過程了,也就是各種調用處理 傳遞數據->構建中綴處理數據->中綴轉后綴->后綴運算顯示

比如點擊了數字

              // 數字:0-9
                    if (!isNaN(parseInt(val, 10))) {
                        // 構建中綴表達式並顯示
                        var infixRe = that.buildInfix(parseInt(val, 10), 'add');
                        that.$in.text(infixRe.join('')).addClass('active');

                        that.calculate();

                        return;
                    }

又比如幾個預運算,其實長得也差不多

                // 預運算:百分比、小數點、平方
                    else if (['per', 'dot', 'sq'].indexOf(action) !== -1) {
                        if (!that.infix.length || that.isOp(that.lastVal)) {
                            return;
                        }

                        if (action === 'per') {
                            that.lastVal /= 100;
                        } else if (action === 'sq') {
                            that.lastVal *= that.lastVal;
                        } else if (action === 'dot') {
                            // that.curDot = true;
                        }

                        // 重新構建中綴表達式
                        var infixRe = that.buildInfix(that.lastVal, 'change');
                        that.$in.text(infixRe.join('')).addClass('active');

                        that.calculate();
                    }

 

以上就是這個簡單計算器的實現步驟了,變化太多還不敢保證不會出錯

基本邏輯如此,如果要加上小數點運算、括號運算、正余弦等科學計算器的功能,還是自己去實現吧。。腦大啊。。

 

  1 $(function() {
  2 
  3     function Calculator($dom) {
  4         this.$dom = $($dom);
  5         // 歷史運算
  6         this.$history = this.$dom.find('.calc-history');
  7         // 輸入區
  8         this.$in = this.$dom.find('.calc-in');
  9         // 輸出區
 10         this.$out = this.$dom.find('.calc-out');
 11         this.$operation = this.$dom.find('.calc-operation');
 12 
 13         // 運算符映射
 14         this.op = {
 15             'plus': '+',
 16             'minus': '-',
 17             'mul': '*',
 18             'div': '/'
 19         };
 20         this.opArr = ['+', '-', '*', '/'];
 21 
 22         // 中綴表達式
 23         this.infix = [];
 24         // 后綴表達式
 25         this.suffix = [];
 26         // 后綴表達式運算結果集
 27         this.result = [];
 28         // 存儲最近的值
 29         this.lastVal = 0;
 30         // 當前已經計算等於完成
 31         this.calcDone = false;
 32         // 當前正在進行小數點點(.)相關值的修正
 33         this.curDot = false;
 34 
 35         this.init();
 36     }
 37 
 38     Calculator.prototype = {
 39         constructor: Calculator,
 40         // 初始化
 41         init: function() {
 42             this.bindEvent();
 43         },
 44         // 綁定事件
 45         bindEvent: function() {
 46             var that = this;
 47 
 48             that.$operation.on('click', function(e) {
 49                 e = e || window.event;
 50                 var elem = e.target || e.srcElement,
 51                     val,
 52                     action;
 53 
 54                 if (elem.tagName === 'TD') {
 55                     val = elem.getAttribute('data-val') || elem.getAttribute('data-ac');
 56                     // 數字:0-9
 57                     if (!isNaN(parseInt(val, 10))) {
 58                         // 構建中綴表達式並顯示
 59                         var infixRe = that.buildInfix(parseInt(val, 10), 'add');
 60                         that.$in.text(infixRe.join('')).addClass('active');
 61 
 62                         that.calculate();
 63 
 64                         return;
 65                     }
 66 
 67                     action = val;
 68 
 69                     // 操作:清除、刪除、計算等於
 70                     if (['cls', 'del', 'eq'].indexOf(action) !== -1) {
 71                         if (!that.infix.length) {
 72                             return;
 73                         }
 74 
 75                         // 清空數據
 76                         if (action === 'cls' || (action === 'del' && that.calcDone)) {
 77                             that.$in.text('');
 78                             that.$out.text('');
 79 
 80                             that.resetData();
 81                         }
 82                         // 清除
 83                         else if (action === 'del') {
 84                             // 重新構建中綴表達式
 85                             var infixRe = that.buildInfix(that.op[action], 'del');
 86                             that.$in.text(infixRe.join('')).addClass('active');
 87 
 88                             that.calculate();
 89 
 90                         }
 91                         // 等於
 92                         else if (action === 'eq') {
 93                             that.calculate('eq');
 94 
 95                         }
 96                     }
 97                     // 預運算:百分比、小數點、平方
 98                     else if (['per', 'dot', 'sq'].indexOf(action) !== -1) {
 99                         if (!that.infix.length || that.isOp(that.lastVal)) {
100                             return;
101                         }
102 
103                         if (action === 'per') {
104                             that.lastVal /= 100;
105                         } else if (action === 'sq') {
106                             that.lastVal *= that.lastVal;
107                         } else if (action === 'dot') {
108                             // that.curDot = true;
109                         }
110 
111                         // 重新構建中綴表達式
112                         var infixRe = that.buildInfix(that.lastVal, 'change');
113                         that.$in.text(infixRe.join('')).addClass('active');
114 
115                         that.calculate();
116                     }
117                     // 運算符:+ - * /
118                     else if (that.isOp(that.op[action])) {
119                         if (!that.infix.length && (that.op[action] === '*' || that.op[action] === '/')) {
120                             return;
121                         }
122 
123                         var infixRe = that.buildInfix(that.op[action], 'add');
124                         that.$in.text(infixRe.join('')).addClass('active');
125                     }
126                 }
127             });
128         },
129 
130         resetData: function() {
131             this.infix = [];
132             this.suffix = [];
133             this.result = [];
134             this.lastVal = 0;
135             this.curDot = false;
136         },
137 
138         // 構建中綴表達式
139         buildInfix: function(val, type) {
140             // 直接的點擊等於運算之后,
141             if (this.calcDone) {
142                 this.calcDone = false;
143                 // 再點擊數字,則進行新的運算
144                 if (!this.isOp(val)) {
145                     this.resetData();
146                 }
147                 // 再點擊運算符,則使用當前的結果值繼續進行運算
148                 else {
149                     var re = this.result[0];
150                     this.resetData();
151                     this.infix.push(re);
152                 }
153 
154             }
155 
156             var newVal;
157 
158             // 刪除操作
159             if (type === 'del') {
160                 newVal = this.infix.pop();
161                 // 刪除末尾一位數
162                 newVal = Math.floor(newVal / 10);
163                 if (newVal) {
164                     this.infix.push(newVal);
165                 }
166 
167                 this.lastVal = this.infix[this.infix.length - 1];
168                 return this.infix;
169             }
170             // 添加操作,首先得判斷運算符是否重復
171             else if (type === 'add') {
172                 // 兩個連續的運算符
173                 if (this.isOp(val) && this.isOp(this.lastVal)) {
174                     return this.infix;
175                 }
176                 // 兩個連續的數字
177                 else if (!this.isOp(val) && !this.isOp(this.lastVal)) {
178                     newVal = this.lastVal * 10 + val;
179                     this.infix.pop();
180                     this.infix.push(this.lastVal = newVal);
181 
182                     return this.infix;
183                 }
184                 // 首個數字正負數
185                 if (!this.isOp(val) && this.infix.length === 1 && (this.lastVal === '+' || this.lastVal === '-')) {
186                     newVal = this.lastVal === '+' ? val : 0 - val;
187                     this.infix.pop();
188                     this.infix.push(this.lastVal = newVal);
189 
190                     return this.infix;
191                 }
192 
193             // TODO: 小數點運算
194             //     if (this.isOp(val)) {
195             //         this.curDot = false;
196             //     }
197 
198             //     // 小數點
199             //     if (this.curDot) {
200             //         var dotLen = 0;
201             //         newVal = this.infix.pop();
202             //         dotLen = newVal.toString().split('.');
203             //         dotLen = dotLen[1] ? dotLen[1].length : 0;
204 
205             //         newVal +=  val / Math.pow(10, dotLen + 1);
206             //         // 修正小數點運算精確值
207             //         newVal = parseFloat(newVal.toFixed(dotLen + 1));
208 
209             //         this.infix.push(this.lastVal = newVal);
210             //         return this.infix;
211             //     }
212 
213                 this.infix.push(this.lastVal = val);
214                 return this.infix;
215             }
216 
217             // 更改操作,比如%的預運算
218             else if (type === 'change') {
219                 this.infix.pop();
220                 this.infix.push(this.lastVal = val);
221 
222                 return this.infix;
223             }
224 
225         },
226         // 判斷是否為運算符
227         isOp: function(op) {
228             return op && this.opArr.indexOf(op) !== -1;
229         },
230         // 判斷運算符優先級
231         priorHigher: function(a, b) {
232             return (a === '+' || a === '-') && (b === '*' || b === '/');
233         },
234         // 進行運算符的運算
235         opCalc: function(b, op, a) {
236             return op === '+'
237                 ? a + b
238                 : op === '-'
239                 ? a - b
240                 : op === '*'
241                 ? a * b
242                 : op === '/'
243                 ? a / b
244                 : 0;
245         },
246         // 即時得進行運算
247         calculate: function(type) {
248             this.infix2Suffix();
249             var suffixRe = this.calcSuffix();
250 
251             if (suffixRe) {
252                 this.$out.text('=' + suffixRe)
253                     .attr('title', suffixRe)
254                     .removeClass('active');
255 
256                 // 如果是直接顯示地進行等於運算
257                 if (type === 'eq') {
258                     this.$in.removeClass('active');
259                     this.$out.addClass('active');
260                     // 設置標記:當前已經顯示地進行計算
261                     this.calcDone = true;
262                     this.lastVal = suffixRe;
263                     // 設置歷史記錄
264                     var history = this.infix.join('') + ' = ' + suffixRe;
265                     this.$history.text(history).attr('title', history);
266                 }
267 
268             }
269         },
270 
271         // 中綴表達式轉后綴
272         infix2Suffix: function() {
273             var temp = [];
274             this.suffix = [];
275 
276             for (var i = 0; i < this.infix.length; i++) {
277                 // 數值,直接壓入
278                 if (!this.isOp(this.infix[i])) {
279                     this.suffix.push(this.infix[i]);
280                 }
281                 else {
282                     if (!temp.length) {
283                         temp.push(this.infix[i]);
284                     }
285                     else {
286                         var opTop = temp[temp.length - 1];
287                         // 循環判斷運算符優先級,將運算符較高的壓入后綴表達式
288                         if (!this.priorHigher(opTop, this.infix[i])) {
289                             while (temp.length && !this.priorHigher(opTop, this.infix[i])) {
290                                 this.suffix.push(temp.pop());
291                                 opTop = temp[temp.length - 1];
292                             }
293                         }
294                            // 將當前運算符也壓入后綴表達式
295                         temp.push(this.infix[i]);
296                     }
297                 }
298             }
299             // 將剩余運算符號壓入
300             while (temp.length) {
301                 this.suffix.push(temp.pop());
302             }
303         },
304 
305         // 后綴表達式計算
306         calcSuffix: function() {
307             this.result = [];
308 
309             for (var i = 0; i < this.suffix.length; i++) {
310                 // 數值,直接壓入結果集
311                 if (!this.isOp(this.suffix[i])) {
312                     this.result.push(this.suffix[i]);
313                 }
314                 // 運算符,從結果集中取出兩項進行運算,並將運算結果置入結果集合
315                 else {
316                     this.result.push(this.opCalc(this.result.pop(), this.suffix[i], this.result.pop()));
317                 }
318             }
319             // 此時結果集中只有一個值,即為結果
320               return this.result[0];
321         }
322     };
323 
324     new Calculator('.calc-wrap');
325 });
完整JS

 


免責聲明!

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



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