写在前边
这是我第一在CSDN发布博客。
在一个月左右的前端基础学习之后,我开始着手自己做一个简单的计算器。
目前实现的功能:加减乘除四则运算,带括号的运算,小数点计算,黑暗/白天模式切换,响应式
UI设计
白天模式
暗黑模式
小窗口
在设计上参考了最近比较火的新拟态设计(并不是特别正规),其实现大致分为
1.凸起按钮
利用右下角的深色阴影和左上角的浅色阴影实现
2.凹陷按钮
利用内部的(inset)左上角深色阴影和内部的右下角浅色阴影实现
3.圆形凸起/凹陷
类似于大头针的感觉的凸起(凹陷感觉是个“坑”的样式),在上述基础上添加一个135度的线性渐变
其实设计上就是模拟左上角的打光实现的阴影让视觉上看起来是凸起或凹陷的,这里有一个网站https://neumorphism.io/#ffffff可以在线生成,弄懂原理之后并不难
代码
接下来是重点的代码部分
目录结构(有能看出来是什么IDE的大神嘛)
calculator.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>计算器</title>
<link rel="stylesheet" href="css/whiteCal.css" id="mode">
<link rel="stylesheet" href="css/calculator.css" ><!--宽度大于1019px生效-->
<link rel="stylesheet" href="css/calculator-mini.css"><!--宽度小于1019px生效-->
</head>
<body>
<div id="changeButtonDiv">
<button type="button" id="change" οnclick="changeStyle()"></button>
</div>
<div id="content">
<div id="windowArea">
<div id="answerArea"></div>
</div>
<div id="operatorArea">
<div id="funcArea">
<div id="clear" class="button">C</div>
<div id="brackets" class="button">()</div>
<div id="backspace" class="button">←</div>
</div>
<div id="numbersArea">
<button type="button" class="button num">1</button><button type="button" class="button num">2</button><button type="button" class="button num">3</button>
<button type="button" class="button num">4</button><button type="button" class="button num">5</button><button type="button" class="button num">6</button>
<button type="button" class="button num">7</button><button type="button" class="button num">8</button><button type="button" class="button num">9</button>
<button type="button" class="button num">.</button><button type="button" class="button num" style="width: 160px;">0</button>
</div>
<div id="computeArea">
<div class="button com" style="clear:left">+</div>
<div class="button com" style="clear:left">-</div>
<div class="button com" style="clear:left">*</div>
<div class="button com" style="clear:left">/</div>
</div>
<button class="button" id="compute">=</button>
</div>
</div>
<script src="js/calculator.js"></script>
</body>
</html>
html的部分没什么好说的,基础的结构,变量名起的比较规范,不需要注释应该也可以看懂
css
css部分采用分离式写法,将有关颜色的部分与大小位置等样式分开写,方便颜色的切换。
实际上还有一种颜色切换的方式,给所有涉及颜色的元素添加一个统一的类,用js控制,黑色添加black类,白色添加white类,再在对应的类下写相应的样式(想法来自后来写的翻页时钟)
正常模式下的calculator.css(只包含大小和样式)
@media screen and (min-width:1019px) {
* {
margin: 0;
padding: 0
}
/**
*计算器面板
*/
#content {
width: 1000px;
height: 700px;
margin: 20px auto;
text-align: center;
border-radius: 50px;
}
/**
*视窗区
*/
#windowArea {
float: left;
width: 500px;
height: 700px;
box-sizing: border-box;
border-radius: 50px 20px 20px 50px;
}
/**
*视窗面板
*/
#answerArea {
width: 450px; height: 650px;
margin: 20px; box-sizing: border-box;
border-radius: 50px;
word-break: break-all;
font-size: 50px; padding-top: 30px;
}
/**
*操作区面板
*/
#operatorArea {
float: right;
width: 500px;
height: 700px;
box-sizing: border-box;
border-radius: 20px 50px 50px 20px;
font-size: 30px;
}
/**
*功能区
*/
#funcArea {
float: left;
margin-left: 40px;
margin-top: 50px;
width: 400px;
}
/**
*符号区
*/
#computeArea {
float: left;
}
/**
*数字区
*/
#numbersArea {
float: left;
margin-left: 40px;
width: 290px;
height: 380px;
border-radius: 100px;
}
/**
*按键
*/
.button {
float: left;
width: 70px;
height: 70px;
line-height: 70px;
margin-left: 20px;
margin-top: 20px;
text-align: center;
font-size: 20px;
border-radius: 100px;
border: none;
outline: none;
}
/*计算符号*/
.com {
border-radius: 15px;
}
/*退格键*/
#backspace {
width: 170px;
margin-left: 30px;
}
/*计算键*/
#compute {
position: relative;
width: 370px;
font-size: 50px;
color: greenyellow;
margin-left: 60px;
border-radius: 15px;
}
/**
*更改主题按钮
*/
#changeButtonDiv {
position: absolute;
left: 50px;
top: 50px;
width: 30px;
height: 60px;
background: white;
border: 5px solid #7a7a7a;
border-radius: 20px;
}
#change {
position: absolute;
top: 0;
width: 30px;
height: 30px;
border-radius: 100px;
border: none;
background: linear-gradient(135deg, #d6d6d6, #c6c6c6);
box-shadow: 1px 1px 3px #3a3a3a;
outline: none;
}
#change:hover {
cursor: pointer;
}
}
小窗口下的calculator-mini.css(不是针对小屏设备的css,仅仅是在缩小了屏幕窗口后的响应式适配)
@media screen and (max-width:1019px) {
* {margin: 0;padding: 0;}
/**
*计算器面板
*/
#content {
width: 50%;
height: 650px;
margin: 20px auto;
text-align: center;
border-radius: 50px;
}
/**
*视窗区
*/
#windowArea {
float: left;
width: 100%;
height: 100px;
box-sizing: border-box;
border-radius: 50px;
overflow: hidden;
}
/**
*视窗面板
*/
#answerArea {
width: 90%; height: 80px;
margin-left:5%; margin-top: 5px;
word-break: break-all;
box-sizing: border-box;
border-radius: 50px;
font-size: 30px; padding-top:5px;
}
/**
*操作区面板
*/
#operatorArea {
float: right;
width: 100%; height: 550px;
box-sizing: border-box;
border-radius: 50px;
font-size: 30px;
overflow: hidden;
}
/**
*功能区
*/
#funcArea {
float: left;
margin-left: 40px;
width: 400px;
}
/**
*符号区
*/
#computeArea {
float: left;
}
/**
*数字区
*/
#numbersArea {
float: left;
margin-left: 40px;
width: 290px;
height: 380px;
border-radius: 100px;
}
/**
*按键
*/
.button {
float: left;
width: 70px;
height: 70px;
line-height: 70px;
margin-left: 20px;
margin-top: 20px;
text-align: center;
font-size: 20px;
border-radius: 100px;
border: none;
outline: none;
}
/*计算符号*/
.com {
border-radius: 15px;
}
/*退格键*/
#backspace {
width: 170px;
margin-left: 30px;
}
/*计算键*/
#compute {
position: relative;
width: 50%; height: 50px;
font-size: 50px; line-height: 50px;
color: greenyellow;
margin-left: 25%; margin-top: 5px;
border-radius: 15px;
}
/**
*更改主题按钮
*/
#changeButtonDiv {
position: absolute;
left: 50px;
top: 50px;
width: 30px;
height: 60px;
background: white;
border: 5px solid #7a7a7a;
border-radius: 20px;
}
#change {
position: absolute;
top: 0;
width: 30px;
height: 30px;
border-radius: 100px;
border: none;
background: linear-gradient(135deg, #d6d6d6, #c6c6c6);
box-shadow: 1px 1px 3px #3a3a3a;
outline: none;
}
#change:hover {
cursor: pointer;
}
}
白天模式的white.css
#content{
border:5px solid rgba(255, 255, 255, 0.72);
box-shadow: 5px 5px 15px #cbcbcb, -5px -5px 15px #f7f7f7;
}
/**
*视窗区
*/
#windowArea{
border:5px solid white;
box-shadow: inset 5px 5px 15px #cacaca, inset -5px -5px 15px #6b6b6b;
background: linear-gradient(135deg, #ffffff, #f1f3f1);
}
/**
*操作区
*/
#operatorArea{
border:5px solid white;
box-shadow: inset 5px 5px 10px #d0d0d0, inset -5px -5px 15px #989898;
background: linear-gradient(135deg, #ffffff, #f1f3f1);
}
/**
*视窗面板
*/
#answerArea{
border: 1px solid #f4f4f4;
background: white;
color: coral;
}
/**
*按键
*/
.button{
color: coral; background: white;
box-shadow: 3px 3px 5px#989898,-3px -3px 5px #d0d0d0;
}
/*按钮悬停样式*/
.button:hover{
background: coral; color: white;
box-shadow: inset 3px 3px 5px #9a4d2f,inset -3px -3px 5px #ffcc99;
}
#compute{
background: #146dff;
}
/*计算键鼠标悬停颜色从左到右填充样式*/
#compute:after,#compute:before{
content:'';
position: absolute; left:0; top:0;
width: 0; height: 100%;
background: #146dff;
z-index: -2;
border-radius:12px ;
}
#compute:hover{z-index: 1; background: transparent; color: #146dff;}
#compute:before{
transition: all 0.5s;
background: aquamarine;
box-shadow: inset 3px 3px 5px #59b294,inset -3px -3px 5px #d0eaff;
z-index: -1;
}
#compute:hover:after,#compute:hover:before{width: 100%;}
暗黑模式dark.css
body{background: #272727}
#content{
border:5px solid #272727;
box-shadow: 5px 5px 15px black, -5px -5px 15px #232323;
background: #272727;
}
/**
*视窗区
*/
#windowArea{
border:5px solid #272727;
box-shadow: inset 5px 5px 15px black, inset -5px -5px 15px #232323;
background: linear-gradient(225deg, #1f1f1f, #101010);
}
/**
*操作区
*/
#operatorArea{
border:5px solid #161616;
box-shadow: inset 5px 5px 10px #393439, inset -5px -5px 15px #232323;
background: linear-gradient(135deg, #262626, #272727);
}
/**
*视窗面板
*/
#answerArea{
border: 1px solid #161616;
background: #272727;
color: #abaeab;
}
/**
*按键
*/
.button{
color: #abaeab; background: #272727;
box-shadow: 3px 3px 5px #232323,-3px -3px 5px #393439;
}
/*按钮悬停样式*/
.button:hover{
background:#272727; color: #abaeab;
box-shadow: inset 3px 3px 5px #232323,inset -3px -3px 5px #393439;
}
#compute{
background: #272727;
}
/*计算键鼠标悬停颜色从左到右填充样式*/
#compute:after,#compute:before{
content:'';
position: absolute; left:0; top:0;
width: 0; height: 100%;
background: #272727;
z-index: -2;
border-radius:12px ;
}
#compute:hover{z-index: 1; background: transparent;}
#compute:before{
transition: all 0.5s;
background: #000000;
box-shadow: inset 3px 3px 5px #232323,inset -3px -3px 5px #393439;
z-index: -1;
}
#compute:hover:after,#compute:hover:before{width: 100%;}
这里多说一句,之前又看过一篇文章,有提到为什么微信迟迟不开放暗黑模式(最近开了,头条的暗黑模式什么时候能上)。
对于我来说,暗黑模式的作用并非在晚上看手机护眼(emmmm...所以近视),只是为了和黑色的果8匹配(奇怪的理由,ios有暗黑模式了,应用也得黑才舒服)。
而实际上,暗黑模式并不仅仅是把界面变黑那么简单。科学研究表明,纯黑色的界面不仅不护眼,更会增加视觉负担,并且使得一些在亮色模式下的阴影、动效等提示用户的功能变得难以察觉。所以实际上应用的暗色模式使用的大多是#191919左右的不同“黑色”(或者说灰色)来凸显阴影、层级以及交互。
同样,这款计算器在配色方面也参考了这一理念(调色让人上瘾)。
calculator.js
/**
* 切换日间/夜间模式
*/
function changeStyle(){
var changeButton=document.getElementById("change");
var bgcolor=document.getElementById("changeButtonDiv");
var Mode=document.getElementById("mode");
var modeStr=Mode.href;
var start=modeStr.lastIndexOf("/")+1;
var end=modeStr.lastIndexOf(".");
var mode=modeStr.slice(start,end);
if (mode=="whiteCal"){
Mode.href="css/darkCal.css";
changeButton.style.top="30px";
bgcolor.style.background="aqua";
}
else {
Mode.href="css/whiteCal.css";
changeButton.style.top="0";
bgcolor.style.background="white";
}
}
/**
*计算
*/
var button=document.getElementsByClassName("button");
var answerArea=document.getElementById("answerArea");
var expression=[];//存值
/**
* 判断视窗区是否为空
* @returns {boolean}
*/
function isEmpty() {
return answerArea.innerText == "";
}
/**
* 为每个按钮添加点击事件并处理
*/
for (let i=0;i<button.length;i++){
button[i].οnclick=handleInput;//添加点击事件
//处理输入
function handleInput() {
var thisValue=this.innerHTML;//被点击的按钮的值(即当前值)
var lastValue=expression[expression.length-1];//将栈顶的值保存(即上一次操作的值)
// 需要用expression.length 而不是length,length=1
if ( !isNaN(thisValue) ) {//如果输入的是数字
displayAndSave(thisValue,!isEmpty());//显示值并保存
if ( !isNaN(lastValue) ) combineNumber(lastValue,thisValue);//如果上一次输入也是数字则合并
else if (lastValue==".") combineFloat(thisValue);//如果上一次输入了“.",则需要处理小数
}
else {//输入的不是数字,而是操作符
if (!isEmpty()) {
switch (thisValue) {
case"←" : backspace(); break;
case "C" : clear(); break;
case "()" : fillBrackets(true); break;
case "=": cal(); break;
default :
//如果上一次输入的是数字或是括号,则可以输入+-*/.,否则报错
if ( !isNaN(lastValue) || lastValue=="(" || lastValue==")" ) displayAndSave(thisValue,true);
else alert("不能连续输入两次运算符!");
break;
}
}
else {//只允许先输入括号
if (thisValue=="()") fillBrackets(false);
else alert("上来就想操作?");
}
}
console.log("输入了"+thisValue+"\t上一次输入了:"+lastValue);
console.log("表达式:"+expression);
}
}
/**
* 处理连续输入的数字
* @param lastValue
* @param thisValue
*/
function combineNumber(lastValue,thisValue) {
var temp="";
temp = lastValue + thisValue;//把上一次和这一次的值合成一个字符串
expression.pop();//弹出thisValue
expression.pop();//弹出lastValue
expression.push(temp);//压入新数
console.log("整数处理后的expression:"+expression);
}
/**
* 处理小数
* @param thisValue
*/
function combineFloat(thisValue) {
var lastLastValue=expression[expression.length-3];//小数点前的数字
// -1是小数点后的数字,-2是小数点,-3是小数点前的数字
var temp="";
console.log("小数点前的数字:"+lastLastValue);
temp=temp+lastLastValue+"."+thisValue;
expression.pop();//弹出小数点后的数字
expression.pop();//弹出小数点
expression.pop();//弹出小数点钱的数字
expression.push(temp);//压入合并后的小数
console.log("小数处理后的expression:"+expression);
}
/**
* 清屏
*/
function clear() {
answerArea.innerHTML="";
expression=[];
}
/**
* 退格
*/
function backspace() {
//表达式的长度为1则清零,否则将截取表达式的第一位到倒数第二位,实现退格
answerArea.innerText=answerArea.innerText.length==1 ? "" : answerArea.innerText.substr(0,answerArea.innerText.length-1);
expression.pop();//删除最末尾的值
}
/**
* 填写括号
*/
function fillBrackets(isSave) {
//没左括号先写左括号,有则写右括号
if ( answerArea.innerText.indexOf("(")==-1 ) displayAndSave("(",isSave);
else displayAndSave(")",isSave);
}
/**
* 将按下的按钮的值显示在answerArea中,并存入expression[]
* @param value 显示的值, isSave 是否保留AnswerArea中的值 : true->在其后显示;false->替换原有值
*/
function displayAndSave(value , isSave) {
expression.push(value);
if (isSave) answerArea.innerHTML +=value;
else answerArea.innerHTML =value;
}
/**
*计算
*/
function cal() {
if (isEmpty()){
alert("先按=?不对吧!")
}
else {
var preExpression=[];
var result=[];//计算结果
var i=0;
var top=0;
var sub=0;
try{
console.log("开始计算");
preExpression=handleExpression();//处理表达式
var length=preExpression.length;
var value=0;
for ( let i=0 ; i<length ; i++ ){//从左向右遍历
if ( !isNaN(preExpression[i]) ){//读取到数字根据其中是否有小数点判断转换类型后压入result
if ( preExpression[i].indexOf(".")==-1 ) value=parseInt(preExpression[i]);
else value=parseFloat(preExpression[i]);
result.push(value);
}
else {//读取到运算符
top=result.pop();//栈顶元素
sub=result.pop();//次顶元素
result.push( compute(top,sub,preExpression[i]) );
}
}
answerArea.innerHTML+="<p>="+result[0]+"</p>"
}
catch {
alert("发生了问题,没得到答案,再来一次试试?");
clear();
}
}
}
/**
* 计算运算符优先级
* @param thisOperator
* @returns {number} 乘除返回2,加减返回1
*/
function computePriority(thisOperator) {
if ( thisOperator=="*" || thisOperator=="/" ) return 2;
else if ( thisOperator=="+" || thisOperator=="-" ) return 1;
else return 0;
}
/**
* 处理表达式
* @returns {[]}
*/
function handleExpression() {
console.log("开始转换表达式。原表达式为:"+expression+",长度为:"+expression.length);
var preExp=[];
var operator=[];
//将中缀表达式转换为前缀表达式
for ( let i=expression.length-1 ; i>=0 ; i-- ){//从右到左遍历expression
if ( !isNaN(expression[i]) ){//读取到操作数,入栈
preExp.push(expression[i]);
console.log("temp="+preExp+"\t operator="+operator);
}
else {//读取到非数字字符
if ( operator=="" || operator[operator.length-1]==")" ){//operator为空或栈顶为 )右括号 则直接入栈
operator.push(expression[i]);
}
else {//operator不为空且栈顶不是)右括号
if ( expression[i]=="(" ){//是左括号
while ( operator[operator.length-1] !== ")" ){
//将operator栈顶元素压入temp直到遇到 )右括号
preExp.push(operator.pop());
}
operator.pop();//弹出)右括号
}
else if ( expression[i]==")" ){//是右括号,直接入栈
operator.push(expression[i]);
}
else {//是运算符
while ( operator[operator.length-1] !== ")" || operator.length>0){//将这个运算符与栈顶的运算符比较优先级,直到栈顶的不是运算符为止
if (computePriority(expression[i]) >= computePriority(operator[operator.length - 1])) {
operator.push(expression[i]);//如果当前运算符优先级大于等于栈顶运算符,直接入栈
break;
}
else {//如果当前运算符优先级低于栈顶运算符,则将栈顶运算符弹出,然后压入temp
preExp.push(operator.pop());
}
}
}
}
}
}
while( operator.length>0 ){//将operator所有元素弹出并压入preExp中
preExp.push(operator.pop());//得到是反向的前缀表达式,在计算结果时从左向右遍历即可
}
console.log("处理后的表达式="+preExp);
return preExp;
}
/**
* 中间计算
* @param top 栈顶元素
* @param sub 次顶元素
* @param operator 运算符
* @returns {number} 结果
*/
function compute(top,sub,operator) {
var result=0;
switch (operator) {
case "+" : result=top+sub ; break;
case "-" : result=top-sub ; break;
case "*" : result=top*sub ; break;
case "/" :
try{
result=top/sub ;
}
catch (e) {
console.log(e.error);
}
break;
}
result=result.toFixed(5);
return result;
}
这是总体的js,全部的代码就是这些了,接下详细解释js中的算法
算法
切换主题
思路
切换主题的实现方式其实没什么好说的,原理就是获取按钮对应的DOM节点,绑定点击事件(onclick),当点击时判断当前的css文件是white还是dark,从而切换主题,同时将自身位置下移,并把背景颜色设为 aqua。
在获取当前css的路径名后需要做一个简单的处理,即截取最后一个“/”和最后一个“.”中间的字符串(可以在获取之后console.log一下css路径,很长的一串,之后就知道该怎么截取了)
对应的代码段
/**
* 切换日间/夜间模式
*/
function changeStyle(){
var changeButton=document.getElementById("change");
var bgcolor=document.getElementById("changeButtonDiv");
var Mode=document.getElementById("mode");
var modeStr=Mode.href;
var start=modeStr.lastIndexOf("/")+1;
var end=modeStr.lastIndexOf(".");
var mode=modeStr.slice(start,end);
if (mode=="whiteCal"){
Mode.href="css/darkCal.css";
changeButton.style.top="30px";
bgcolor.style.background="aqua";
}
else {
Mode.href="css/whiteCal.css";
changeButton.style.top="0";
bgcolor.style.background="white";
}
}
处理输入
思路
要想做好计算器,第一步就是知道用户输入了什么,同时反馈出相应的处理。
这是我在GoodNotes上写的一个简单的实现方法,原理是在每一次输入时对比上一次输入,得到以下几种情况:
1.这一次是数字
(1).上一次是数字 => 将上一次输入的数字弹出,与这次输入拼接成一个数字字符串(也可以将第一个*10再加第二个数,将字符串变成数字),再压入回表达式(栈)。
(2).上一次是运算符 或 ()=> 压入表达式。
(3).上一次是小数点. => 将上上次的数字弹出(由于(1)的处理,上上次的数字一定是处理成多位数的结果),弹出的数字与小数点和这一次的输入合并后压入表达式。
2.这一次是运算符
(1).上一次是运算符 => 不允许输入(也可以单独考虑 - 这一特殊情况,允许 + 后跟随 - ,将其视为负数 ,但我没这么做)。
(2).上一次是数字或() => 压入表达式。
3.特殊的()
由于布局考虑,只给了()一个按钮的位置,那么如何实现输入括号呢?
解决方法是判断目前的表达式是否有( ,有就写),没有写(。得到的结果是只能写一对()。最好的方式其实是()分别占两个按键,或者说给一个变量flag,起始为true,判断flag为true时写( 并把flag置为false,flag为false时输出 ),再将flag设为true。后续改进。
输入数字、小数点、括号对应的代码段
/**
* 处理连续输入的数字
* @param lastValue
* @param thisValue
*/
function combineNumber(lastValue,thisValue) {
var temp="";
temp = lastValue + thisValue;//把上一次和这一次的值合成一个字符串
expression.pop();//弹出thisValue
expression.pop();//弹出lastValue
expression.push(temp);//压入新数
console.log("整数处理后的expression:"+expression);
}
/**
* 处理小数
* @param thisValue
*/
function combineFloat(thisValue) {
var lastLastValue=expression[expression.length-3];//小数点前的数字
// -1是小数点后的数字,-2是小数点,-3是小数点前的数字
var temp="";
console.log("小数点前的数字:"+lastLastValue);
temp=temp+lastLastValue+"."+thisValue;
expression.pop();//弹出小数点后的数字
expression.pop();//弹出小数点
expression.pop();//弹出小数点钱的数字
expression.push(temp);//压入合并后的小数
console.log("小数处理后的expression:"+expression);
}
/**
* 填写括号
*/
function fillBrackets(isSave) {
//没左括号先写左括号,有则写右括号
if ( answerArea.innerText.indexOf("(")==-1 ) displayAndSave("(",isSave);
else displayAndSave(")",isSave);
}
对于特殊操作的处理
特殊操作是指退格、清屏
退格
退格的思路是 判断当前表达式长度是否为1,为1则清零,否则pop()并截取字符串到倒数第二位
/**
* 退格
*/
function backspace() {
//表达式的长度为1则清零,否则将截取表达式的第一位到倒数第二位,实现退格
answerArea.innerText=answerArea.innerText.length==1 ? "" : answerArea.innerText.substr(0,answerArea.innerText.length-1);
expression.pop();//删除最末尾的值
}
清屏
清屏的思路是 将表达式数组清空,同时将视窗区清空
/**
* 清屏
*/
function clear() {
answerArea.innerHTML="";
expression=[];
}
处理表达式
https://blog.csdn.net/antineutrino/article/details/6763722这篇文章详细讲述了前、中、后缀表达式以及转换的算法。
这是我在GoodNotes上的笔记。我的计算器使用的是前缀表达式(emmm...处理之后的结果是后缀,再将元素弹出到新栈就是前缀,前缀计算时需要从右到左遍历表达式,我从左到右遍历的,所以结果一样,看不懂请忽略这句非人话)。大体思路就是蓝色字体所写的。
接下来通过1+((2+3)*4)-5这个例子解释算法:
1.准备两个栈。 放中间结果的temp[] 和 放运算符的operator[]
2.从右到左读取表达式,也就是 5 - )4 * )3 + 2 ( ( + 1
3.接下来判断
(1)遇到5 => temp[] 。 此时 temp=[5] , operator=[]
(2)遇到- => operator 为空 => operator[] 。 此时temp=[5] , operator=[-]
(3)遇到)=> operator[] 。 此时temp=[5] , operator=[ - , ) ]
(4)遇到4 => temp[] 。此时temp=[5 , 4 ] , operator=[ - , ) ]
(5)遇到* =>operator[length-1]为)=> operator[] 。 此时temp=[ 5 , 4 ] , operator=[ - , ) , * ]
(6)遇到)=> operator[] 。 此时temp=[ 5 , 4 ] , operator=[ - , ) , * , ) ]
(7)遇到3 => temp[] 。此时temp=[5 , 4 , 3 ] , operator=[ - , ) , * , ) ]
(8)遇到+ => operator[length-1]为)=> operator[] 。 此时temp=[ 5 , 4 , 3 ] , operator=[ - , ) , * , ) , + ]
(9)遇到2 => temp[] 。此时temp=[ 5 , 4 , 3 , 2 ] , operator=[ - , ) , * , ) , + ]
(10)遇到( => pop + ->temp[] => pop ) 。此时 temp=[ 5 , 4 , 3 , 2 , + ] , operator=[ - , ) , * ]
(11)遇到( => pop * ->temp[] => pop ) 。此时 temp=[ 5 , 4 , 3 , 2 , + , * ] , operator=[ - ]
(12)遇到+ => operator[length-1]为+,优先级相同=> operator[] 。 此时temp=[ 5 , 4 , 3 , 2 , + , * ] , operator=[ - , + ]
(13)遇到1 => temp[] 。此时temp=[ 5 , 4 , 3 , 2 , + , * , 1 ] , operator=[ - , + ]
(14)operator[] => temp[] 。 temp=[ 5 , 4 , 3 , 2 , + , * , 1 , + , - ]
一共14步,其实不难,但写起来有些复杂(很磨耐性)。
对应的代码段
/**
* 处理表达式
* @returns {[]}
*/
function handleExpression() {
console.log("开始转换表达式。原表达式为:"+expression+",长度为:"+expression.length);
var preExp=[];
var operator=[];
//将中缀表达式转换为前缀表达式
for ( let i=expression.length-1 ; i>=0 ; i-- ){//从右到左遍历expression
if ( !isNaN(expression[i]) ){//读取到操作数,入栈
preExp.push(expression[i]);
console.log("temp="+preExp+"\t operator="+operator);
}
else {//读取到非数字字符
if ( operator=="" || operator[operator.length-1]==")" ){//operator为空或栈顶为 )右括号 则直接入栈
operator.push(expression[i]);
}
else {//operator不为空且栈顶不是)右括号
if ( expression[i]=="(" ){//是左括号
while ( operator[operator.length-1] !== ")" ){
//将operator栈顶元素压入temp直到遇到 )右括号
preExp.push(operator.pop());
}
operator.pop();//弹出)右括号
}
else if ( expression[i]==")" ){//是右括号,直接入栈
operator.push(expression[i]);
}
else {//是运算符
while ( operator[operator.length-1] !== ")" || operator.length>0){//将这个运算符与栈顶的运算符比较优先级,直到栈顶的不是运算符为止
if (computePriority(expression[i]) >= computePriority(operator[operator.length - 1])) {
operator.push(expression[i]);//如果当前运算符优先级大于等于栈顶运算符,直接入栈
break;
}
else {//如果当前运算符优先级低于栈顶运算符,则将栈顶运算符弹出,然后压入temp
preExp.push(operator.pop());
}
}
}
}
}
}
while( operator.length>0 ){//将operator所有元素弹出并压入preExp中
preExp.push(operator.pop());//得到是反向的前缀表达式,在计算结果时从左向右遍历即可
}
console.log("处理后的表达式="+preExp);
return preExp;
}
计算
最后一步,也是最关键的一步就是计算了。
从左到右读取表达式,遇到操作数将其压入result[]中,读取到运算符就将栈顶和次顶元素进行相应的运算,其中可以判断一下表达式,有小数点将字符串parseFloat ,没有就parseInt。由于JavaScript的浮点数计算不精确,所以在小数计算中将其结果toFixed(5),保留5位小数
计算对应的代码
按下=触发事件,开始计算
/**
*计算
*/
function cal() {
if (isEmpty()){
alert("先按=?不对吧!")
}
else {
var preExpression=[];
var result=[];//计算结果
var i=0;
var top=0;
var sub=0;
try{
console.log("开始计算");
preExpression=handleExpression();//处理表达式
var length=preExpression.length;
var value=0;
for ( let i=0 ; i<length ; i++ ){//从左向右遍历
if ( !isNaN(preExpression[i]) ){//读取到数字根据其中是否有小数点判断转换类型后压入result
if ( preExpression[i].indexOf(".")==-1 ) value=parseInt(preExpression[i]);
else value=parseFloat(preExpression[i]);
result.push(value);
}
else {//读取到运算符
top=result.pop();//栈顶元素
sub=result.pop();//次顶元素
result.push( compute(top,sub,preExpression[i]) );
}
}
answerArea.innerHTML+="<p>="+result[0]+"</p>"
}
catch {
alert("发生了问题,没得到答案,再来一次试试?");
clear();
}
}
}
中间计算对应的代码段
/**
* 中间计算
* @param top 栈顶元素
* @param sub 次顶元素
* @param operator 运算符
* @returns {number} 结果
*/
function compute(top,sub,operator) {
var result=0;
switch (operator) {
case "+" : result=top+sub ; break;
case "-" : result=top-sub ; break;
case "*" : result=top*sub ; break;
case "/" :
try{
result=top/sub ;
}
catch (e) {
console.log(e.error);
}
break;
}
result=result.toFixed(5);
return result;
}
在处理除法时可以单独考虑除数为零的情况然后throw抛出错误,我这里直接try catch了
其他的想判断运算符优先级,判断视窗区是否为空的函数就不赘述了。
总结
主观来说,这个计算器还阔以,基本完成作为计算器应该做的事,而其实比起算法让我更感兴趣的是界面的设计。
客观来说,这个计算器比较草率,功能少,缺少更完善的处理以及更丰富的功能,同时在应对用户不同的输入情况没有继续细分,加深了用户的学习成本,希望日后有时间可以改善。
总的说来,这次动手做个计算器也是对于阶段性学习的一个总结,书本和IDE上亲手打的代码终究还是千差万别,错误也往往存在于细节之中,而只有亲手去做,才能获得更好的理解。(不得不吐槽学校的教学方式,老师一个劲将,学生也没什么实践的机会,实践课老师也就坐着像看晚自习......)