寫在前邊
這是我第一在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" onclick="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].onclick=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上親手打的代碼終究還是千差萬別,錯誤也往往存在於細節之中,而只有親手去做,才能獲得更好的理解。(不得不吐槽學校的教學方式,老師一個勁將,學生也沒什麼實踐的機會,實踐課老師也就坐着像看晚自習......)