首页 > 代码库 > JS实现简易的计算器
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">←</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 + b238 : op === ‘-‘239 ? a - b240 : op === ‘*‘241 ? a * b242 : op === ‘/‘243 ? a / b244 : 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实现简易的计算器