最近想用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等地方的错误处理也没有。希望大家看的时候能注意下。
本人是一只小白,上述部分如有错漏之处,或者说有更好的思路、方法请在评论区指出