函式發展入門:從重複程式碼到靈活的工具

Published July 21, 2025 by 徐培鈞
JavaScript

在學習程式設計的過程中,你可能會遇到一個重要的問題:什麼時候該使用函式?

函式(Function)就像是程式中的小工具,能幫助你整理重複的程式碼、提高可讀性,甚至在未來應對更多變化。

本文將以一個簡單的農場數字列印案例為例,帶你逐步體驗函式設計的思考過程——從寫死的程式碼,到結構更合理、可重複利用的函式。

為什麼需要函式?

在寫程式的時候,你一定會慢慢發現:光靠一行一行寫,很快就會亂掉

程式一多、功能一複雜,就會開始覺得「這段邏輯好像重複好幾次」或「這裡應該有個專門的功能才對」。

這時候,函式就派上用場了。它就像是幫你整理房間的小抽屜,可以把一段邏輯收起來,想用的時候再拿出來。

下面這兩種情況最常讓人自然想到「我應該寫個函式」。

發現自己寫了好多重複的程式碼

🔍 什麼意思?

這種情況超常見:你在不同地方,不斷重複做一模一樣的事情。

比如說:

  • 在數字前補 0
    像是列印發票或農場動物數量時,7 要補成 00711 要補成 011
  • 計算價格加稅
    每次結帳時都要算一次總金額,可能好幾個頁面都用到同樣的公式。
  • 格式化日期
    2025/7/21 轉成 2025-07-21,結果這段轉換程式碼在五個地方都複製過。

一開始專案小的時候你可能覺得沒關係,「複製貼上最快」。

但當專案變大、程式碼到處散落,你會開始覺得頭痛,因為只要邏輯一有改動,你得在好幾個地方重複修正。

⚠️ 為什麼不好?

  1. 改起來超麻煩
    只要有一天,你想把補 0 的邏輯從「補到三位數」改成「補到四位數」,
    你得去翻找所有有用到這段程式碼的地方,一個一個改。如果漏掉其中一個地方,結果就會不一致,可能還會產生奇怪的 bug。
  2. 超難維護
    半年後你回來維護專案,可能完全忘記「哪一段是最新版本」。
    假設你有三個地方都在計算價格加稅,但其中一個是舊公式,另外兩個是新公式,你會被自己搞瘋。
  3. 增加理解負擔
    其他人(甚至未來的你自己)要看懂這段程式碼,就得每次都重新讀一遍整段邏輯,浪費時間。

✅ 把它包進函式

最好的做法就是把這種重複邏輯「收進一個小盒子裡」,也就是寫成一個函式。

比如說,你可以寫一個 zeroPad(number, width) 函式,專門負責補 0。

以後只要需要補 0,不用再重複寫一堆 while 迴圈,直接呼叫它就好。

console.log(zeroPad(7, 3));  // 007
console.log(zeroPad(11, 3)); // 011

好處是什麼?

  • 改邏輯只要改一個地方
  • 其他人一看函式名稱就知道用途
  • 讓主要程式碼看起來更乾淨、易讀

想到一個功能,但還沒寫出來

🔍 什麼意思?

有時候你還沒開始寫程式,就已經知道「這裡應該要有個功能」。

你可以先「取一個有意義的名字」放著,就像先插一個旗子,告訴自己「這裡之後要補這段邏輯」。

例子:

  • 「我要列印農場的動物數量」→ 先寫一個 printFarmInventory() 放著,之後再決定裡面要怎麼印。
  • 「我要計算購物車總價」→ 先寫 calculateTotalPrice(),就算現在還沒想好怎麼寫,也不會打亂主要流程。

✅ 為什麼要先取名字?

  1. 幫助思考、理清流程
    你可以先把整個流程寫出來,不用卡在細節上。就像先畫一張地圖,細節以後慢慢補。
  2. 讓程式更好讀
    就算你今天只寫好主流程,其他人(還有未來的你自己)一看函式名稱,就能大概猜到它的作用。
  3. 避免「寫到一半忘了自己在幹嘛」
    當你一次專注在主要結構,再慢慢回頭寫細節,寫程式會更有條理。

小結

總結一句話:

  • 遇到重複的程式碼 → 把它包成函式,省事又安全。
  • 腦中已經想好功能 → 先取個好名字,保持流程乾淨。

函式就像幫你整理邏輯的小工具,越早習慣這種思維,程式就會越好維護。

第一個版本:寫死的簡單函式

目標

我們要做一件很簡單的事:

列印農場動物的數量,並且在數字前補 0,確保數字永遠是三位數。

最終想要得到的輸出是這樣:

007 Cows
011 Chickens

也就是說:

  • 7 頭牛 → 變成 007
  • 11 隻雞 → 變成 011

實作:第一次嘗試

來看看最直接的做法:

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);

  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}

printFarmInventory(7, 11);

程式做了什麼?

這段程式其實很簡單,但如果你是剛開始學程式,可能會好奇「為什麼要這樣寫」。讓我們一步一步拆開來看

把數字轉成字串

let cowString = String(cows);
  • 這行做的事是:把原本的數字(7)轉成字串("7")。
  • 為什麼要這樣做?
    因為我們要用 .length 來檢查字串長度,而數字是沒有 .length 這個屬性的。
  • 換句話說,我們是為了後面方便處理「字串長度」才轉型的

💡 初學者常見問題

很多人會問「為什麼不直接操作數字?」

原因是數字要補 0 很麻煩,你得用數學方式計算,還得考慮進位。但字串就簡單多了,只要在前面一直加 "0" 就好。

用 while 不斷補 0

while (cowString.length < 3) {
  cowString = "0" + cowString;
}
  • 這段邏輯的意思是:
    只要字串長度還沒到 3,就一直在最前面加一個 "0"
  • 例如:
  • 第一次檢查:"7""07"
  • 第二次檢查:"07""007"
  • 第三次檢查:長度已經是 3,跳出迴圈。

為什麼要用 while

因為我們不知道最一開始的數字有幾位數,可能是 7(1 位)、11(2 位)、甚至以後可能是 123(3 位)。

while 能幫我們重複動作,直到長度符合需求。

💡 初學者常見問題

有人會問「那為什麼不用 if?」

因為 if 只能判斷一次,遇到 7 這種需要補兩次 0 的情況就不行了,while 才能確保補到剛剛好。

印出結果

console.log(`${cowString} Cows`);
  • 最後用模板字串(`${變數} 文字`)列印結果。
  • 輸出的效果:
  007 Cows
  • 模板字串的好處:比起寫 console.log(cowString + " Cows"),看起來更直覺。

成果驗收:程式能跑起來

執行後,得到我們想要的結果:

007 Cows
011 Chickens

到這一步,一切看起來都沒問題,目標達成!

但這樣有什麼問題?

雖然現在功能正常,但這種寫法只適合「小玩具專案」。

當需求變複雜,你會很快被自己的程式碼拖垮。

每新增一種動物,就得複製一大段程式碼

現在只有牛和雞還好,但老闆如果再養豬、鴨、羊,你就得一直複製貼上這幾行「轉字串 + while 補 0 + 列印」。

重複這麼多次,不僅麻煩,還會讓程式碼越來越長。

規則改動要改好幾個地方

假設老闆說:「我想把動物數量改成四位數,例如 0007。」

你得去每個動物的 while 迴圈裡< 3 改成 < 4,一不小心漏改一個就出錯。

補 0 的邏輯寫了好幾次,很亂也容易搞錯

這種邏輯最好只有一個版本。如果寫在多個地方,哪天你改壞其中一個,結果輸出就可能不一致。

小結

所以,雖然這是最簡單、最直觀的寫法,但它不適合長期維護

接下來我們就要思考:

👉 能不能把「補 0」這段邏輯抽出來,寫成一個獨立的小工具函式?
👉 這樣新增動物或改規則時,就不用到處修改程式了。

改善重複程式碼:第一步優化

重新思考:可以把「補 0」的邏輯抽出來嗎?

在上一個版本,我們為了列印每種動物的數量,重複寫了很多相似的程式碼:

  • 把數字轉字串
  • while 補 0
  • console.log 印出結果

這樣雖然能用,但問題很明顯:

每次要新增一種動物,就得複製貼上一大段幾乎相同的程式碼。

👉 所以我們開始重新思考:

「這段重複的邏輯能不能抽出來?能不能寫一次就到處用?」

答案是:可以,把它封裝成函式。

改寫後的程式

function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);

  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }

  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);

🔍 改進版的執行流程

printFarmInventory(7, 11, 3) 為例,程式其實做了這些事:

  1. 呼叫 printZeroPaddedWithLabel(cows, "Cows")
  • 把數字 7 丟進去
  • 函式幫你自動轉成 "007"
  • 印出:007 Cows
  1. 呼叫 printZeroPaddedWithLabel(chickens, "Chickens")
  • 11 丟進去
  • 自動轉成 "011"
  • 印出:011 Chickens
  1. 呼叫 printZeroPaddedWithLabel(pigs, "Pigs")
  • 3 丟進去
  • 自動轉成 "003"
  • 印出:003 Pigs

最後得到這樣的結果:

007 Cows
011 Chickens
003 Pigs

這樣做有什麼好處?

重複的邏輯被「收進一個盒子」裡了

以前要寫好幾行才能「補 0 + 印出」,現在只要呼叫一次 printZeroPaddedWithLabel 就能搞定。

這就像是把工具收進工具箱裡,用到時直接拿出來,不用每次都從頭造工具

增加新動物超方便

要新增一個動物,只要再呼叫一次函式就好,例如:

printZeroPaddedWithLabel(sheep, "Sheep");

完全不需要再寫一次補 0 的邏輯。

換句話說,你現在只要專心處理資料,不用再去想「我要怎麼補 0」。

主要程式碼看起來更乾淨

printFarmInventory 現在變得超簡單,看起來就像一份「動物清單」:

printZeroPaddedWithLabel(cows, "Cows");
printZeroPaddedWithLabel(chickens, "Chickens");
printZeroPaddedWithLabel(pigs, "Pigs");

很直覺、很好讀,你一眼就能看懂「這裡在做什麼」。

不足的地方(為什麼還能再優化?)

雖然這樣已經好很多,但還有兩個小問題

  1. 函式名稱太長
    printZeroPaddedWithLabel 這名字很精確,但又臭又長,打起來很煩。
  2. 一個函式同時做兩件事
  • 幫數字補 0
  • 直接印出結果

這樣會有一個問題:如果未來我只想補 0,但不想印出來,這個函式就不能直接用。

所以我們下一步會再把「補 0」拆成一個獨立的小函式。

進一步精煉:單一職責的函式

到這裡,我們的程式已經比一開始好很多:

  • 重複邏輯被抽出來
  • 增加新動物只要多呼叫一個函式

但仍有一個問題:

👉 我們的 printZeroPaddedWithLabel 仍然在做兩件事:「補 0」+「印出結果」。

🔍 為什麼要讓函式只做一件事?

在程式設計中,有一個非常重要的好習慣:
「一個函式最好只負責一件事」(這也被稱為「單一職責原則」)。

原因很簡單:

  1. 更靈活
    如果補 0 的功能被獨立出來,我們可以在其他地方(不只是列印動物數量)直接重用它。
  2. 更好維護
    以後要改補 0 的規則(例如補到 5 位數),只要改一個地方。
  3. 更好理解
    一個函式名稱就代表一個明確的功能,讀起來就像在讀一個清單,而不是一大段混雜邏輯。

所以,我們要把「補 0」獨立成自己的函式,並且讓它只專注於回傳補好 0 的字串

終極版本程式

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

執行結果:

007 Cows
016 Chickens
003 Pigs

終極版本做了什麼?(一步步拆解思路)

這個最終版本的核心,就是把程式分工做得更清楚,每個函式只專心做好一件事

我們來細拆每一步。

zeroPad(number, width):專心補 0

這個函式就像一個「小工具」,專門幫你把數字格式化成你想要的樣子。

👉 這個函式到底做了什麼?
  1. String(number) → 把數字轉成字串
  • 例如 7"7"16"16"
  • 為什麼要這樣做?
    因為字串才有 .length 屬性,我們才能用 .length 來檢查目前的位數。
  1. while (string.length < width) → 持續補 0
  • 只要字串長度還沒達到我們指定的寬度(width),就不斷在前面加 "0"
  • 例子:
    • 7"7""07""007"
    • 16"16""016"(補一次就夠了)
  • 為什麼用 while 而不是 if
    if 只能判斷一次,如果數字只有 1 位(需要補 2 個 0),if 只會補一次,結果會錯。
    while 能確保「補到剛剛好」。
  1. return string → 回傳結果
  • 最後這個函式只會回傳處理好的字串,不做其他事。
  • 例子:
    • zeroPad(7, 3)"007"
    • zeroPad(16, 3)"016"

printFarmInventory:專心列印清單

在這個版本裡,printFarmInventory 不再關心補 0 的細節,它唯一的工作就是「把數字和標籤一起印出來」。

👉 它現在的樣子:
console.log(`${zeroPad(cows, 3)} Cows`);
console.log(`${zeroPad(chickens, 3)} Chickens`);
console.log(`${zeroPad(pigs, 3)} Pigs`);

這樣一眼看過去,就像在讀一個簡單的「動物清單」,而不是一大堆重複的補 0 邏輯。

✅ 好處是什麼?
  • 你不用再關心「補 0 要寫幾行、邏輯怎麼寫」。
  • 你只要相信 zeroPad 會給你正確的結果。

這樣做有什麼好處?

zeroPad 是萬用小工具

因為它只專心處理字串格式,所以它不限定在動物數量,任何需要補 0 的地方都能直接拿來用:

  • 產生編號(001, 002, 003
  • 格式化日期(例如把 7"07",方便印成 2025-07-21
  • 製作整齊的報表數字(對齊效果更好看)

維護更輕鬆

如果哪天需求變了,老闆說「我要把數字補到 5 位數(例如 00007)」——

只要修改 zeroPad 一行邏輯,所有用到它的地方都會自動更新

不用再像第一版那樣跑去每個地方逐一修改。

程式可讀性大幅提升

printFarmInventory 現在變得非常乾淨,看起來就像一段「流程描述」:

console.log(`${zeroPad(cows, 3)} Cows`);
console.log(`${zeroPad(chickens, 3)} Chickens`);
console.log(`${zeroPad(pigs, 3)} Pigs`);

即使是第一次看這段程式碼的人,也能馬上猜到:

「哦,這裡是印出動物數量,而且數字會被格式化成 3 位數。」

函式責任分工更明確

  • zeroPad → 只負責處理數字格式(像是專門的「工具人」)
  • printFarmInventory → 只負責輸出結果(像是「記錄員」)

這種分工結構,就像在搭積木:

每塊積木功能單純、好組合,以後還能重複利用。

小結:這就是「好程式」的樣子

從最初的「把所有東西擠在一起」,到最後的「每個函式各司其職」,你應該能感受到:

程式更容易理解
新增功能或改需求更簡單
函式能重複使用,真正變成工具

這就是為什麼「單一職責的函式」是程式設計中非常重要的一個好習慣。

函式設計思維:該多抽象?該多聰明?

當你開始會寫函式後,很容易陷入一個誤區:是不是能抽出來的邏輯,就一定要抽?

事實上,並不是所有程式碼都需要被封裝成函式。函式的目的是讓程式更好維護,而不是為了「顯得自己很專業」。

接下來我們來看看在設計函式時,應該怎麼拿捏這個「抽象程度」。

適度聰明:別為了「炫技」過度抽象

zeroPad 是一個很好的例子,它簡單、明確、而且用途很廣,所以非常值得封裝成函式。

但有些初學者一學會寫函式,就會過度抽象,像這樣:

function addOneAndPrintHello() {
  console.log("Hello");
  return 1 + 1;
}

這種函式就完全沒必要,因為:

  • 它只會用到一次
  • 功能非常簡單,一眼就能看懂
  • 別人看到這個函式名稱,還得打開來看它到底在幹嘛,反而增加理解成本

💡 一句話總結:函式應該讓程式「更簡單」,而不是「更複雜」。

判斷原則:什麼時候該封裝成函式?

你可以用以下兩個問題來幫自己判斷:

  1. 這段邏輯會被重複使用嗎?
  • ✅ 會 → 封裝成函式
  • ❌ 不會 → 留在當前程式碼就好
    **例子:**補 0、計算稅額、格式化日期 → 很常用,所以值得封裝。
  1. 這段邏輯有「明確意圖」嗎?
  • ✅ 有 → 封裝成函式,名稱就像「標籤」一樣,幫助理解
  • ❌ 沒有 → 抽出來只會讓程式更難讀
    例子:printFarmInventory → 一看就知道「列印農場動物數量」。

💡 一個小技巧
當你可以用一句話清楚表達這段邏輯是「做什麼事」時,就適合把它封裝成函式,並用這句話當作它的名稱。

找到平衡點:什麼該抽?什麼不該抽?

該抽出來的:

  • 通用邏輯 → 多次使用、到處都會用到
    例如:zeroPad()formatDate()calculateTax()
  • 具有明確意圖的功能 → 即使只用一次,但能讓程式更好讀
    例如:printFarmInventory()(一看就知道它是「列印農場清單」的功能)

不該抽出來的:

  • 只用一次、邏輯非常簡單
    例如:let total = a + b; 不需要寫一個 addTwoNumbers(a, b)
  • 抽象到過於零碎
    例如把「印一行結果」單獨抽出來成 printLine(),除非它有更多附加功能,否則這種封裝只會增加負擔。

結語

從這個簡單的例子中,你應該能感受到函式的力量。

  • 第一步: 解決當下問題
  • 第二步: 減少重複
  • 第三步: 抽出可重用的邏輯,讓程式更靈活

當你未來寫程式時,記得隨時觀察自己的程式碼是否出現重複,並思考:

「這段邏輯會在其他地方用到嗎?能否抽成一個好名字的函式?」