最近看到一則面試題目,要求使用angularjs實現一個計算器,利用放假時間實現了一個仿iOS8風格的計算器,功能基本和iOS自帶的計算器是一致的。
查看demo,接着給出實現過程。
首先創建angularjs的基本項目就不說了,最好是利用yeoman這個腳手架工具直接生成,如果沒有該環境的,當然也可以通過自行下載angularjs的文件引入項目。
main.js是項目的主要js文件,所有的js都寫在這個文件中,初始化之后,該文件的js代碼如下
angular .module('calculatorApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch' ]) .controller('MainCtrl', function ($scope) { $scope.result=""; $scope.data={ "1":["AC","+/-","%","÷"], "2":["7","8","9","×"], "3":["4","5","6","-"], "4":["1","2","3","+"], "5":["0",".","="] }; });
這里的result是用來雙向綁定顯示運算結果的,data為計算器鍵盤上的數字和符號。
該項目相關的所有css代碼如下:
*{ margin:0; padding:0; } body { padding-top: 20px; padding-bottom: 20px; } h1{ text-align:center; color:#3385ff; } .main{ margin:20px auto; border:1px solid #202020; border-bottom: none; width:60%; height:600px; } .result{ display: block; width: 100%; height: 30%; background:#202020; box-sizing: border-box; border:none; padding: 0; margin: 0; resize: none; color: #fff; font-size: 80px; text-align: right; line-height: 270px; overflow: hidden; background-clip: border-box; } .row{ height: 14%; background: #d7d8da; box-sizing: border-box; border-bottom: 1px solid #202020; overflow: hidden; } .col{ height: 100%; box-sizing: border-box; border-right:1px solid #202020; float: left; color: #202020; font-size: 28px; text-align: center; line-height: 83px; } .normal{ width: 25%; } .end-no{ width: 25%; border-right: none; background: #f78e11; color: #fff; } .zero{ width: 50%; } .history{ background:#3385ff ; color:#fff; font-size: 22px; text-align: center; }
然后是html的布局如下:
<body ng-app="calculatorApp" > <h1>calculator for ios8</h1> <hr/> <p class="history">{{ history.join(" ") }}</p> <div class="main"> <textarea ng-model="result" class="result" ></textarea> <div ng-repeat="item in data" class="row"> <div class="col" ng-repeat="a in item" ng-class="showClass($index,a)" ng-click="showResult(a)">{{ a }}</div> </div> </div> </body>
這里class為history的p標簽是用來顯示輸入記錄的,就是說你按下的所有鍵都會顯示在上面,便於查看結果,history為當前scope下面的一個數組,后面會講解。這里使用一個textarea來作為計算結果的顯示屏幕,主要是為了使用雙向綁定的特性。同時生成計算器各個按鍵和界面元素都是通過對data對象進行 循環遍歷來生成的,showClass方法是scope下面的一個方法,用來獲取不規則界面顯示元素的class屬性,后面會講解,showResult方法就是對按鍵響應的主方法,我們所有對按鍵的按下響應都是通過這個方法來的,后面會詳細講解。
showClass方法代碼如下:
//顯示計算器樣式 $scope.showClass=function(index,a){ if(a==0){ return "zero"; } return index==3||a=="="?"end-no":"normal"; };
這個方法主要是針對每行的最后一列要顯示為橘黃色和對於顯示0的按鍵要占用兩個單元格來進行特殊處理。
到目前為止,已經完全實現了計算器的界面,結果如下:

下面需要實現對按鍵的響應,按鍵包括數字鍵,運算符鍵,AC鍵,每種按鍵按下都會有不同相應並且按鍵之間是存在聯系的
為了使代碼容易講解,采用分段性給出showResult方法的代碼然后進行詳細解釋的方法。
首先,這里要添加幾個變量進行控制和存儲之用。
//計算時用的數字的棧 $scope.num=[]; $scope.history=[]; //接受輸入用的運算符棧 $scope.opt=[]; //計算器計算結果 $scope.result=""; //表示是否要重新開始顯示,為true表示不重新顯示,false表示要清空當前輸出重新顯示數字 $scope.flag=true; //表示當前是否可以再輸入運算符,如果可以為true,否則為false $scope.isOpt=true;
num數組實際上是一個棧,用來接收用戶輸入的數字,具體用法后面會講解,history數組為用戶輸入的所有按鍵,每次按下就讓該按鍵上的符號或數字進棧,然后使用綁定實時顯示在界面上。opt數組是另外一個棧,用來接收用戶輸入的運算符。具體用法后面會講解,flag是一個標志,為true的時候表示在按下數字的過程中被按下的數字是當前顯示數字的一部分,需要跟在其后面顯示,比如當前界面顯示的是12,再按下3的時候會判斷該標志,如果為true,就顯示123,否則就清空界面,直接顯示3.isOpt是另外一個標志,主要是為了防止用戶在輸入過程中對運算符的非法輸入,比如說用戶接連輸入了1+2+,當輸到這里是,下面輸入的應該是一個數字,但是用戶卻輸入了一個運算符,通過判斷這個標志,會讓計算器忽略這個非法的運算符,讓輸入依然保持1+2+。
下面的代碼分段給出,完整的代碼就是將它們連接起來。
$scope.init=function(){ $scope.num=[]; $scope.opt=[]; $scope.history=[]; $scope.flag = true; $scope.isOpt=true; } ; $scope.showResult=function(a){ $scope.history.push(a); var reg=/\d/ig,regDot=/\./ig,regAbs=/\//ig; //如果點擊的是個數字 if(reg.test(a)) { //消除凍結 if($scope.isOpt==false){ $scope.isOpt=true; } if ($scope.result != 0 && $scope.flag && $scope.result != "error") { $scope.result += a; } else { $scope.result = a; $scope.flag = true; } }
init方法是用來初始化一些變量和標志,讓它們回到原始狀態。showResult方法是顯示界面響應用戶操作的主方法,上面的代碼是該方法中的一個if分支,表示如果輸入的是一個數字,那么如果對運算符的輸入已經被凍結(當前不允許輸入運算符了,輸入后會被忽略),那么輸入數字的時候,就解開凍結狀態,以便下次輸入運算符的時候會進入運算符棧。如果當前顯示的結果不為空並且現在按下的數字是當前顯示的數字的一部分並且沒有發生錯誤,那么顯示的結果就是當前按下的數字接在當前顯示數字的末尾,否則就代表重新顯示,重新顯示的時候需要讓下次再輸入的數字接在這個數字后面顯示。
js代碼(接上)
//如果點擊的是AC else if(a=="AC"){ $scope.result=0; $scope.init(); }
如果點擊的是AC,那么代表初始化,讓顯示結果為0,清空所有狀態。
js代碼(接上)
//如果點擊的是個小數點 else if(a=="."){ if($scope.result!=""&&!regDot.test($scope.result)){ $scope.result+=a; } }
如果點擊的是個小數點,則在當前顯示不為空並且當前顯示的結果里面不存在小數點的情況下讓這個小數點接在當前顯示的末尾。
js代碼(接上)
//如果點擊的是個取反操作符 else if(regAbs.test(a)){ if($scope.result>0){ $scope.result="-"+$scope.result; } else{ $scope.result=Math.abs($scope.result); } }
如果點擊的是個取反操作,則將當前顯示結果取反
js代碼(接上)
//如果點擊的是個百分號 else if(a=="%"){ $scope.result=$scope.format(Number($scope.result)/100); }
如果點擊的是個百分號,則將當前顯示結果除以100之后再顯示,這里有個format函數,其代碼如下:
//格式化result輸出 $scope.format=function(num){ var regNum=/.{10,}/ig; if(regNum.test(num)){ if(/\./.test(num)){ return num.toExponential(3); } else{ return num.toExponential(); } } else{ return num; } }
它的作用主要是ios8自帶的計算器不會無限顯示很多位的數字,如果超過10位(包括小數點),則采用科學計算法來顯示,這里為了簡便,對於含有小數點且超過10位的顯示結果采用科學計算法計算的時候,讓它保留小數點之后3位顯示。
js代碼(showResult部分接上)
//如果點擊的是個運算符且當前顯示結果不為空和error else if($scope.checkOperator(a)&&$scope.result!=""&&$scope.result!="error"&&$scope.isOpt){ $scope.flag=false; $scope.num.push($scope.result); $scope.operation(a); //點擊一次運算符之后需要將再次點擊運算符的情況忽略掉 $scope.isOpt=false; }
這個分支是最復雜的一個分支,它代表如果輸入的是一個運算符,那么就要進行運算了。進入到這個分支,需要首先將flag置為false,作用是下次再輸入數字就是重新輸入數字而不是接着當前顯示結果輸入了。
然后要讓當前顯示的數字作為被運算的數字首先進入到數字棧中,operation方法就是運算方法,因為這次已經點擊了一個運算符,所以下次再點擊就要忽略這個運算符,將isOpt置為false。
operation代碼如下
//比較當前輸入的運算符和運算符棧棧頂運算符的優先級 //如果棧頂運算符優先級小,則將當前運算符進棧,並且不計算, //否則棧頂運算符出棧,且數字棧連續出棧兩個元素,進行計算 //然后將當前運算符進棧。 $scope.operation=function(current){ //如果運算符棧為空,直接將當前運算符入棧 if(!$scope.opt.length){ $scope.opt.push(current); return; } var operator,right,left; var lastOpt=$scope.opt[$scope.opt.length-1]; //如果當前運算符優先級大於last運算符,僅進棧 if($scope.isPri(current,lastOpt)){ $scope.opt.push(current); } else{ operator=$scope.opt.pop(); right=$scope.num.pop(); left=$scope.num.pop(); $scope.calculate(left,operator,right); $scope.operation(current); } };
該方法接受當前輸入的運算符作為參數,其核心思想為,當前接收到了一個運算符,如果運算符棧為空,則將當前運算符入棧,然后這種情況就不用再做什么了。如果當前運算符棧不為空,那么彈出當前運算符棧的棧頂元素,讓當前接收的運算符和棧頂運算符比較優先級(乘除優先級大於加減,同一優先級的情況下棧頂運算符優先級較高,因為先入棧的)。isPri方法用來判斷優先級的,接收兩個參數,第一個為當前接收的運算符,第二個為出棧的棧頂運算符,如果按照前面所說的規則,當前運算符的優先級較高,那么就直接將這個運算符入棧。如果當前運算符優先級小於棧頂運算符,那么就需要進行計算並更改計算器的顯示了,將運算數字棧棧頂兩個元素依次彈出,分別作為一次運算的兩個運算數字,然后彈出運算符棧的棧頂元素,作為本次運算的運算符,調用calculate方法進行運算,該方法代碼如下
//負責計算結果函數 $scope.calculate=function(left,operator,right) { switch (operator) { case "+": $scope.result = $scope.format(Number(left) + Number(right)); $scope.num.push($scope.result); break; case "-": $scope.result = $scope.format(Number(left) - Number(right)); $scope.num.push($scope.result); break; case "×": $scope.result = $scope.format(Number(left) * Number(right)); $scope.num.push($scope.result); break; case "÷": if(right==0){ $scope.result="error"; $scope.init(); } else{ $scope.result = $scope.format(Number(left) / Number(right)); $scope.num.push($scope.result); } break; default:break; } };
該方法接受三個參數,左運算數字,中間的運算符和右邊的運算數字,按照加減乘除法運算后更改result顯示結果並將計算結果入棧到運算數字棧中,這里需要注意如果運算的是除法並且除數是0,則發生了錯誤,顯示錯誤,清空所有狀態,否則正常運算。
一次運算完成之后,運算符棧和數字棧中的狀態都會被更改,而目前的按鍵current值還沒有入棧,所以又要重復上述過程進行優先級比較后在運算,實際上是一個遞歸的過程,直到運算符棧為空或者當前運算符的優先級高於運算符棧的棧頂運算符。isPri方法是用來判斷運算符優先級的,代碼如下:
//判斷當前運算符是否優先級高於last,如果是返回true //否則返回false $scope.isPri=function(current,last){ if(current==last){ return false; } else { if(current=="×"||current=="÷"){ if(last=="×"||last=="÷"){ return false; } else{ return true; } } else{ return false; } } };
判斷規則前面已經講述。
此外還有一個checkOperator方法,是判斷輸入的符號是不是加減乘除四則運算符號,代碼如下:
//判斷當前符號是否是可運算符號 $scope.checkOperator=function(opt){ if(opt=="+"||opt=="-"||opt=="×"||opt=="÷"){ return true; } return false; }
如果是就返回true,否則返回false。
到目前為止,還有一個輸入等於號的分支沒有,其代碼如下(接showResult方法)
//如果點擊的是等於號 else if(a=="="&&$scope.result!=""&&$scope.result!="error"){ $scope.flag=false; $scope.num.push($scope.result); while($scope.opt.length!=0){ var operator=$scope.opt.pop(); var right=$scope.num.pop(); var left=$scope.num.pop(); $scope.calculate(left,operator,right); } } };
如果輸入的是等於號,則首先將flag置為false,允許下次輸入數字的時候界面重新顯示,並且要將當前顯示的數字作為運算數字入棧到數字棧。然后就要進行不斷的出棧運算直到運算符棧為空才能夠停止。
上面就是實現的主要代碼和過程,由於分支代碼較多而一次全部給出所有分支又不能夠詳細講述,所以將showResult方法分開了,可能看着不太適應,這里是完整代碼以及demo查看地址。由於寫的比較倉促且沒有花太多時間去測試,可能存在一些bug,歡迎指出。同時由於水平有限,可能該方法不是最好,歡迎給出更好的方案一起交流學習~~
