距離完賽已經過了一陣子,前天想自己刻刻看計算機,拆解任務、實際執行後才發現知識量不足,導致無法順利完成。
因為閱讀文字時常覺得很複雜、看不懂,我自己在學習上比較喜歡透過影片的方式(有時候直接看英文影片的吸收力,比讀中文文章更好XD) 這次的計算機也是跟著這位講師一起實作!
在此附上我在實作時的步驟、知識點與心得跟大家分享。如果未來有餘力,也許會再把這些錄成影片。
github // github page
在開始前要先釐清需求,我們要做出的畫面大致像是這樣:
畫面操作影片
之前聽廖洧杰老師分享,說嘗試把大任務拆成中任務、小任務,再開始逐步完成每個任務,可以幫忙釐清自己到底是哪個部分出問題?是知識量不夠?還是有 bug ?因此我也將這樣的建議貫徹進練習當中。
以上是我在看影片以及實際做完後,拆出的任務。大家也可以嘗試自己思考看看,製作計算機會需要達到哪些功能?跟我還未開始前,自己拆解出的任務兩相比較:我本來想做出的畫面更為簡單,因此任務拆解時並沒有列出小數點以及 backspace 的功能。同時,我也沒有想到數字可能超出螢幕這種 bug 。
以下就帶著大家一步一步完成計算機!(大致都直接照著講師的程式打,少部分做一點點調整)
html 應該算是最簡單也基礎的部分了。這裡,我們沒有套用任何框架。
首先,導入<link rel="stylesheet" href="all.css">
和<script src="all.js"></script>
。打一個標題<h1>HTML Calculator</h1>
。
因為計算機的畫面看起來就像欄位一樣,因此用類似 bootstrap 的概念,在外層包 .container ,內層一行用一個 .row 包。加上結果顯示欄位共六行,包六次。 .row 的裡頭包 .column 代表每一格。
除了最上方的結果欄位,其他按鍵都跟計算有關,幫他們設 .calc-btn。同時,為了區分數字鍵和運算符號鍵,再各自設一個 class 名稱:.calc-btn-num 和 .calc-btn-operator 。
當然也別忘了幫每格都設置專屬的 id 。並且在 div 中打上按鍵上該出現的字。 backspace 符號是以 ⇤ 表示,÷ 以 ÷ 表示。
最後你就可以得到下方的程式碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="all.css">
<title>計算機</title>
</head>
<body>
<h1>HTML Calculator</h1>
<div class="container">
<div class="row">
<div class="column" id="calc-display-val">0</div>
</div>
<div class="row">
<div class="calc-btn column" id="calc-clear">AC</div>
<div class="calc-btn column" id="calc-backspace">⇤</div> <!--backspace btn-->
<div class="calc-btn calc-btn-operator column" id="calc-divide">÷</div> <!--divide ÷ btn-->
</div>
<div class="row">
<div class="calc-btn calc-btn-num column" id="calc-seven">7</div>
<div class="calc-btn calc-btn-num column" id="calc-eight">8</div>
<div class="calc-btn calc-btn-num column" id="calc-nine">9</div>
<div class="calc-btn calc-btn-operator column" id="calc-mutiply">x</div>
</div>
<div class="row">
<div class="calc-btn calc-btn-num column" id="calc-four">4</div>
<div class="calc-btn calc-btn-num column" id="calc-five">5</div>
<div class="calc-btn calc-btn-num column" id="calc-six">6</div>
<div class="calc-btn calc-btn-operator column" id="calc-minus">-</div>
</div>
<div class="row">
<div class="calc-btn calc-btn-num column" id="calc-one">1</div>
<div class="calc-btn calc-btn-num column" id="calc-two">2</div>
<div class="calc-btn calc-btn-num column" id="calc-three">3</div>
<div class="calc-btn calc-btn-operator column" id="calc-plus">+</div>
</div>
<div class="row">
<div class="calc-btn calc-btn-num column" id="calc-zero">0</div>
<div class="calc-btn column" id="calc-decimal">.</div>
<div class="calc-btn calc-btn-operator column" id="calc-equals">=</div>
</div>
</div>
<script src="all.js"></script>
</body>
</html>
接著進行 css 的部分。我自己是用 scss 去做,影片中的講師則是使用 css 。有些 css 設定他直接使用 id 處理,基於想著重練習 js 我是沒有特別花時間改掉,這部份如果想要可以再自己調整的更漂亮一些。
首先 import google font:@import url('https://fonts.googleapis.com/css?family=Roboto:100');
,在 body 做字型設定。
為了不要讓計算機上的文字被反白,有另外設了
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
不讓文字反白, google 關鍵字是 css do not highlight 。
外層 .container 用 flex 讓整個計算機置中。 .row .column 分別用 display: table;
與 display: table-cell;
去做,並在 .row 設定寬度與 table-layout: fixed;
。
計算機按鈕 .calc-btn 設定長寬、顏色、背景、文字置中、 hover 等樣式,還可以加上 cursor: pointer;
讓使用者體驗更好。結果顯示欄位也是類似的流程,設定高度、顏色、背景、 hover 等樣式。為了不要讓數字超出欄位很難看,要加上
overflow: hidden; //數字輸入太多時不要超過欄位
white-space: nowrap; //數字輸入太多時不要換行
因為按鈕 0 和 backspace 鍵我們想讓他比較大顆,所以額外設定他的寬度。最後,在結果欄、 0 和 = 設定圓弧,讓計算機四個角正好是圓的。其他細部的 css 樣式我就不一一贅述,可以參考下方的 scss 程式碼:
@import url('https://fonts.googleapis.com/css?family=Roboto:100');
body{
font-family: "Roboto","sans-serif";
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
.container{
display: flex;
flex-direction: column;
align-items: center;
}
.calc-btn{
background: silver;
color: #000;
width: 25px;
height: 45px;
border: 1px solid gray;
text-align: center;
cursor: pointer;
font-size: 32px;
font-weight: 100;
padding-top: 3px;
&:hover{
background: orange;
}
}
.row{
display: table;
table-layout: fixed;
width: 200px;
}
.column{
display: table-cell;
}
#calc-zero, #calc-clear{
width: 52.6666667px;
}
#calc-zero{
border-radius: 0 0 0 7px;
}
#calc-display-val{
height: 80px;
color: white;
text-align: right;
border-left: 1px solid gray;
border-right: 1px solid gray;
border-top: 1px solid gray;
font-size: 48px;
background: #383838;
overflow: hidden; //數字輸入太多時不要超過欄位
white-space: nowrap; //數字輸入太多時不要換行
padding: 12px; //結果欄位的數字跟欄位的padding
border-radius: 7px 7px 0 0;
}
.calc-btn-operator{
background: orange;
color: white;
}
h1{
text-align: center;
margin-top: 50px;
}
#calc-equals{
border-radius: 0 0 7px 0;
}
終於要進入主題的部分。
宣告每個數字按鈕、小數點、 AC 、 backspace 鍵、顯示結果區。
前面在做 html 時,有額外將數字與運算符號分兩種 class ,這個部分也需要做宣告。如此一來,等等在做監聽時就可以把符合 class 名稱的資料,藉由這個宣告,分別放進名為 calcNumBtns 與 calcOperatorBtns 的陣列中。等到有需要時再取出。
const oneBtn = document.getElementById("calc-one");
const twoBtn = document.getElementById("calc-two");
const threeBtn = document.getElementById("calc-three");
const fourBtn = document.getElementById("calc-four");
const fiveBtn = document.getElementById("calc-five");
const sixBtn = document.getElementById("calc-six");
const sevenBtn = document.getElementById("calc-seven");
const eightBtn = document.getElementById("calc-eight");
const nineBtn = document.getElementById("calc-nine");
const zeroBtn = document.getElementById("calc-zero");
const decimalBtn = document.getElementById("calc-decimal");
const clearBtn = document.getElementById("calc-clear");
const backspaceBtn = document.getElementById("calc-backspace");
const displayValElement = document.getElementById("calc-display-val");
let calcNumBtns = document.getElementsByClassName("calc-btn-num");
let calcOperatorBtns = document.getElementsByClassName("calc-btn-operator");
先從需求倒回去想:我們要讓每個數字跟運算按鈕在點擊時,能夠紀錄相對應的數字,進一步顯示以及運算 > 這些按鍵每個都要被監聽,監聽後執行某個函式,再用這個函式讓他做顯示與運算等等我希望他做的事情。
當然可以每個按鍵寫一次 addEventListener ,但這樣很累。從這裡立刻聯想到可以使用 for 迴圈,讓他每個數字都執行一次監聽。設定 i 一開始為 0 ,當 i 小於 calcNumBtns.length 時,就讓 calcNumBtns[i] 能夠在點擊時,執行 updateDisplayVal 的函式。
那 calcNumBtns.length 會是多少呢?因為上面有宣告 calcNumBtns 就是所有 class 名稱為 calc-btn-num 的, html 在數字的地方都有設這個 class ,因此 0-9 共 10 個數字,都會執行監聽。
運算鍵監聽邏輯相同,只是要執行的函式因為不同,另外命名為 performOperation ,其他過程在此不做贅述。
for(let i=0; i < calcNumBtns.length; i++){
calcNumBtns[i].addEventListener("click",updateDisplayVal,false)
}
for(let i=0; i < calcOperatorBtns.length; i++){ //這裡的解釋跟上方監聽數字按鈕相同
calcOperatorBtns[i].addEventListener("click",performOperation,false)
}
接著當然就是設置 updateDisplayVal 與 performOperation 兩個函式。因為我們會最後才處理 performOperation ,建議可以先將剛剛上面寫的
for(let i=0; i < calcOperatorBtns.length; i++){ //這裡的解釋跟上方監聽數字按鈕相同
calcOperatorBtns[i].addEventListener("click",performOperation,false)
}
註解起來,等等要寫 performOperation 這個函式時,再把註解拿掉,避免測試接下來的功能時報錯、無法執行。
為了使按按鍵時,按下的數字被記錄到一個變數中,我們另外宣告一個變數 btnText 。要讓這個變數等於按下的按鍵的內文,會運用到 event Object 和 event target 的觀念。當事件被觸發,瀏覽器會為事件主動創造一個物件,這個物件會包含事件相關的所有資訊例如按鍵按了什麼 keyCode 、滑鼠點了哪裡 clientX 、事件名稱 type 、觸發事件元素 target 、事件是否在冒泡階段觸發 bubbles 等等,每個事件物件提供的屬性會根據觸發的事件不同。要取出屬性資訊,需要提供一個參數。
在這裡因為要讓執行這個函式時,將按下的按鍵中的數字,帶回 btnText 這個變數中,因此幫參數取名為 clickObj , btnText 等於按下觸發事件時的參數(target)的內文(innerHTML)。如此便將按下的數字存起來了。
let updateDisplayVal = (clickObj) =>{
let btnText = clickObj.target.innerHTML;
}
要使上方結果欄位預設顯示為 0 ,並讓數字打完後可存起來,需要宣告幾個變數:
剛剛的函式還沒做完,回過頭來處理一下。顯示結果區的變數,要等於目前已經顯示在結果區的數字,加上 btnText 紀錄起來的數字。全部加好的數再讓 displayValElement 顯示。
let displayVal = "0";
let pendingVal = ""; //
let evalStringArray = [];
let updateDisplayVal = (clickObj) =>{
let btnText = clickObj.target.innerHTML;
displayVal += btnText;
displayValElement.innerHTML = displayVal;
}
還有一個小細節,我們不想讓計算機上方結果欄出現 012345 之類的數字,因此需要放一個 if 判斷式進來。如果 displayVal === "0" ,讓他變成空字串(也就是空白,不要秀 0 ,只秀 12345 )。
let updateDisplayVal = (clickObj) =>{
let btnText = clickObj.target.innerHTML;
if(displayVal === "0"){
displayVal = '';
}
displayVal += btnText;
displayValElement.innerHTML = displayVal;
}
接著要完成 AC 鍵功能,可以使用 .onclick 來處理。一樣,先釐清需求並拆解任務:我們想要讓使用者按下這個按鈕時,能夠讓顯示區變數變回預設值 0 ,本來儲存的變數 pendingVal 清空,運算區變數 evalStringArray 也要回到預設值,最後讓顯示區的內文等同顯示區變數。
於是你就得到下方的程式碼:
clearBtn.onclick = () => {
displayVal = "0"; //按下時顯示區變數為0
pendingVal = undefined; //本來儲存的pendingVal刪除
evalStringArray = []; //運算區清空
displayValElement.innerHTML = displayVal; //顯示區等同顯示區變數
}
pendingVal 的部分宣告了,但沒有要存的數字,因此是 undefined 。
製作 backspace 鍵,目的是要讓最後一個數字被刪除,細部拆解任務和需求後還會加上一項:刪到最後一個數字後,上方顯示區要變回預設的 0 ,而不是一個數字都不剩變空白畫面。
於是會用到 slice(begin,end) 的觀念,它可以用來操控陣列和字串,作用是將陣列中的東西複製一份出來。 begin 為從哪開始複製, end 為複製到哪之前要結束。如果沒寫 end ,則得到所有元素。寫負數時,代表從後面開始算起。若想要看例題,可以點擊這裡。
由此可知我們會需要先用 .length 取得變數 displayVal 目前的數字長度(因為使用者每次打的數字長度可能不同),運用 slice 讓他從第一筆資料抓到倒數第一筆資料前。如此一來就會刪掉最後一筆資料,達到 backspace 的作用。
設 if 判斷式,如果 displayVal 已經是空字串,讓 displayVal 變為 0 。
backspaceBtn.onclick = () =>{
let lengthOfDisplayVal = displayVal.length;
displayVal = displayVal.slice(0, lengthOfDisplayVal -1);
if(displayVal === ""){
displayVal = "0";
}
displayValElement.innerHTML = displayVal;
}
不知道有沒有人發現,剛剛在 html 標籤裡,小數點並沒有設置到 .calc-btn-num 。其實我們也可以幫它設定這個 class ,但是這樣會讓小數點可以無限次打上去,例如 100.3.234.34 這種不合理的數字。因此這裡直接取用它的 id ,幫它設一個它自己專屬的函式。
我們的任務是讓小數點顯示在顯示區變數中,以及如果已經顯示過,就不要再顯示第二次。換句話說,點擊 . 這個按鈕時,如果顯示區變數還沒出現過 . ,則讓顯示區數字加上 . 。這裡用到 .includes 的觀念。我之前不知道變數錢也可以加上 ! 作為不的意思,這次從這裡學習到了。
decimalBtn.onclick = () => {
if(!displayVal.includes('.')){
displayVal +=".";
}
displayValElement.innerText = displayVal;
}
終於要回來做 performOperation ,若剛剛有先註解起來別忘了取消。
一樣是利用 .target ,將按下的運算符號帶回operator這個變數中,幫參數取名為 clickObj , operator 等於按下(觸發事件)時的參數的內文(innerHTML)。如此便將按下的運算符號存起來了。
接著會利用** switch 判斷式**,判斷按下的是哪種運算符號,決定要做怎樣的運算。 switch 判斷式不難,主要是在 case 後面加上條件,如果達成條件就執行下方函式,並搭配 break 強迫終止不繼續做下方判斷。 switch 還可以設定 default ,當上面所有條件都不符合時,就執行 default 。我做了一個簡單的例題可供還不太懂的人參考。
判斷是加減乘除之後又要讓他幹嘛呢?當然就是要讓他進行運算囉!運算的部分,可以使用 eval() 來做,他會將傳入的字串當作 js 指令來執行,如果傳入的是數字就會進行加減乘除的運算。
仔細拆解任務後,我們會想到幾件要做的事情:並不是按完加減乘除後就要立刻得到運算結果,而是按完等於後才做。我們應該要先把本來顯示在 displayVal 變數的資料存放到 pendingVal ,接著清空 displayVal 讓他顯示 0 ,讓 0 顯示到畫面上,把 pendingVal push 進 evalStringArray 陣列中,最後把加號也 push 進去。
最後才去利用 evalStringArray 陣列做 eval() 運算。就這樣依序設定完加減乘除。
在等號的鍵,因為使用者按完最後一組數字後,不會再按加減乘除,而是直接按等號,所以要把還在 displayVal 變數裡的資料先 push 進 evalStringArray 陣列。接著讓陣列 evalStringArray 執行 eval() ,並運用 join() 的觀念,它的作用在於將陣列中所有元素連接合併成一個字串(例如本來是 ['1' '+' '2'] 會變成 '1+2'),並回傳。我們需要宣告一個變數 evaluation ,讓做完以上這些事情的資料可以帶到這個變數中。再讓剛剛執行完的 evaluation 以字串顯示,並重新成為 displayVal 變數。執行後 evalStringArray 也要記得清空,下次才能以空白陣列重新紀錄新的值。
let performOperation = (clickObj) => {
let operator = clickObj.target.innerHTML;
switch(operator){
case '+':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('+');
break;
case '-':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('-');
break;
case 'x':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('*');
break;
case '÷':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('/');
break;
case '=':
evalStringArray.push(displayVal);
let evaluation = eval(evalStringArray.join(' '));
displayVal = evaluation + '';
displayValElement.innerText = displayVal;
evalStringArray = [];
break;
default:
break;
}
}
這樣我們就完成了所有的功能!完整的程式碼如下:
const oneBtn = document.getElementById("calc-one");
const twoBtn = document.getElementById("calc-two");
const threeBtn = document.getElementById("calc-three");
const fourBtn = document.getElementById("calc-four");
const fiveBtn = document.getElementById("calc-five");
const sixBtn = document.getElementById("calc-six");
const sevenBtn = document.getElementById("calc-seven");
const eightBtn = document.getElementById("calc-eight");
const nineBtn = document.getElementById("calc-nine");
const zeroBtn = document.getElementById("calc-zero");
const decimalBtn = document.getElementById("calc-decimal");
const clearBtn = document.getElementById("calc-clear");
const backspaceBtn = document.getElementById("calc-backspace");
const displayValElement = document.getElementById("calc-display-val");
let calcNumBtns = document.getElementsByClassName("calc-btn-num");
let calcOperatorBtns = document.getElementsByClassName("calc-btn-operator");
let displayVal = "0";
let pendingVal = "";
let evalStringArray = [];
let updateDisplayVal = (clickObj) =>{
let btnText = clickObj.target.innerHTML;
if(displayVal === "0"){
displayVal = '';
}
displayVal += btnText;
displayValElement.innerHTML = displayVal;
}
let performOperation = (clickObj) => {
let operator = clickObj.target.innerHTML;
switch(operator){
case '+':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('+');
break;
case '-':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('-');
break;
case 'x':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('*');
break;
case '÷':
pendingVal = displayVal;
displayVal = '0';
displayValElement.innerText = displayVal;
evalStringArray.push(pendingVal);
evalStringArray.push('/');
break;
case '=':
evalStringArray.push(displayVal);
let evaluation = eval(evalStringArray.join(' '));
displayVal = evaluation + '';
displayValElement.innerText = displayVal;
evalStringArray = [];
break;
default:
break;
}
}
for(let i=0; i < calcNumBtns.length; i++){
calcNumBtns[i].addEventListener("click",updateDisplayVal,false)
}
for(let i=0; i < calcOperatorBtns.length; i++){
calcOperatorBtns[i].addEventListener("click",performOperation,false)
}
clearBtn.onclick = () => {
displayVal = "0";
pendingVal = undefined;
evalStringArray = [];
displayValElement.innerHTML = displayVal;
}
backspaceBtn.onclick = () =>{
let lengthOfDisplayVal = displayVal.length;
displayVal = displayVal.slice(0, lengthOfDisplayVal -1);
if(displayVal === ""){
displayVal = "0";
}
displayValElement.innerHTML = displayVal;
}
decimalBtn.onclick = () => {
if(!displayVal.includes('.')){
displayVal +=".";
}
displayValElement.innerText = displayVal;
}
這是我一邊製作時,一邊用 git commit 紀錄的製作過程
希望大家還滿意這篇文章!