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">←</td> <td data-ac="sq">x<sup>2</sup></td> <td data-ac="mul">×</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">÷</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; }
這樣靜態的計算器就粗來了~~
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 });