# Day 12 - JavaScript - code review、數值判斷、readline 模組化 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2025-12-05 **URL:** https://paragraph.com/@gcake/day-12 ## Content Code Review 的幾種形式Code review 的核心目標是提升程式碼品質、分享知識、找出潛在問題。實務上常見的形式可分為:1. 自我 review(隔一段時間回頭看)時機:完成一段功能後,提交給別人看之前。做法:先放下手邊工作幾分鐘,再回來用「陌生人」的視角重新審視程式碼 diff,檢查是否有明顯的邏輯錯誤、錯字或不一致的命名。優點:能過濾掉最基本的錯誤,提升自己產出的品質,也讓後續的 review 更有效率。2. 和 AI 討論 review時機:需要快速得到「語法、風格、明顯 bug」的檢查;或想要「其他寫法」的靈感。做法:將一小段程式碼或一個檔案交給 AI,並清楚說明程式的目的與你的問題,例如:「幫我找出這段程式碼中可以重構的地方」或「幫我想想還有哪些測試情境我沒考慮到」。優點:即時性高,能快速找出「機器眼」擅長發現的模式問題,也是很好的 brainstorm 夥伴。3. 同儕或 mentor review(Peer Review)時機:團隊開發的標準流程(例如 GitLab MR / GitHub PR)。做法:將改動整理成清楚的 commit 或 MR,附上修改目的、核心變更與對應測試。Reviewer 會從架構、可維護性、是否符合團隊規範等角度給出建議。優點:能藉由不同人的觀點找出自己的盲點,特別是在業務邏輯、架構設計與長期維護性上。4. 結對 / Over-the-shoulder review時機:遇到較複雜的問題,或在設計一個新功能時,需要即時討論。做法:兩個人(或多人)共用一個螢幕或線上共編工具,一人負責寫,其他人邊看邊給建議。本質上是「即時的 code review」,隨時調整寫法。優點:回饋最即時,適合用在釐清需求、設計 API 或解決棘手問題的階段。Review 內容 checklist不論用哪種形式,都可以從這三大塊內容檢查:語法與風格命名是否語意清楚?(例如 data、temp、item 這類命名是否可以更具體?)是否使用 === 而非 == 來避免非預期的型別強制轉換?是否可以用可選串連(?.)或空值合併運算子(??)來簡化 null 或 undefined 的檢查?演算法與設計是否有過於複雜的巢狀迴圈?時間複雜度是否合理?在需要高效能查詢的場景,是否可以用 Map 或 Set 的 has() 取代陣列的 find() 或 includes()?public API 的定義是否合理?模組與模組之間的界線是否清楚?測試與邊界測試是否只測了 happy path(正常情境),而忽略了邊界值(例如 0、-1、空陣列)或錯誤輸入?測試的 mock 是否過多,導致在測「mock 本身」而不是「真實邏輯」?測試名稱是否清楚描述「規格」(it('should return sum when given a positive integer')),而不是只寫「case 1」?適合使用 AI 協助 code review 的情境AI 很適合處理「機器擅長、但人類覺得瑣碎」的情境,可以當作 pre-review 或輔助教練。開發中「自己先過一輪」的 pre-review情境:剛寫完一小段功能,丟給同事 review 前。AI 用途:快速掃描語法錯誤、未使用的變數、重複的邏輯、過長的函式,並提醒缺少哪些測試情境(例如邊界、錯誤輸入)。練習題或 side project 尋求靈感情境:像現在練習 TDD、演算法題目。AI 用途:問「同一題有沒有其他寫法?」、「這段程式碼可以怎麼重構?」、「這份測試規格還可以怎麼拆解?」,當作 brainstorm 夥伴。處理大量重複、模板化的改動情境:在多個檔案中加入同樣的 log、錯誤處理,或修改 API 名稱。AI 用途:幫忙檢查是否有檔案漏改、拼錯字,或協助產生重構後的樣板程式。理解複雜或老舊的程式碼情境:接手別人的專案,或看到一段很長的函式。AI 用途:請它用自然語言摘要「這支函式在做什麼」,或畫出 pseudo-code,幫助你快速建立 mental model。產生安全性 / 效能的提示清單情境:處理使用者輸入、檔案 I/O、API 呼叫等潛在風險點。AI 用途:請它列出「這種情境常見的安全性或效能問題」,當作一份動態 checklist 來逐項檢查。不那麼適合只靠 AI review 的情境涉及複雜商業規則的決策。需要團隊共識的架構設計或風格取捨。在這類情境,AI 可以提供選項,但最終還是要由人類做出決策。今天進行了第一次 code review 下面列出這次發現前兩週的程式碼可以再改善的部分:用 try...catch...finally 顯示友善訊息在 CLI 裡的典型流程:import { divideBy3Times } from './q9.js'; import readline from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; const rl = readline.createInterface({ input, output }); async function main () { const num = await rl.question('請輸入一個正整數:'); const n = parseInt(num, 10); try { const times = divideBy3Times(n); console.log(`${n} 至少要除以 3 連續 ${times} 次,小數點後第二位四捨五入後是零`); } catch (error) { console.log('輸入錯誤,請輸入一個大於 0 的整數'); } finally { rl.close(); } } main(); domain 函式(例如 divideBy3Times)在輸入不合法時 throw new Error(...)。try...catch 負責把錯誤轉成「使用者看得懂的訊息」,避免未捕捉例外直接中斷程式。finally 確保 rl.close() 一定會被呼叫,釋放 readline 的資源。參考:流程控制與例外處理 參考:Errors | Node.js 參考:Readline | Node.js變數宣告與命名方法避免 magic number:重要數字用常數或已宣告變數表達(例如 const MAX_LINES = 2)。語意化命名:變數用名詞、函式用動詞,名稱可以長,但要一眼看得出用途。let / const 原則:預設用 const,只有真的需要重新指定(reassign)才改用 let。這樣可以在語法層幫忙擋掉部分 bug。參考:語法與型別數值方法Number(n) 與 parseInt(n, 10)Number(n):將整個值轉成 number,若字串不是完整合法數字,結果會是 NaN。parseInt(n, 10):會從左到右解析字串中的整數部分,例如 "123abc" → 123,"abc" → NaN。在 CLI 題目中,常見做法是:使用 parseInt(num, 10) 把輸入字串轉成整數或 NaN。再搭配 Number.isInteger(n)、n > 0 檢查是否是合法正整數。參考:Number 參考:parseInt()isNaN 與 Number.isNaNisNaN(x)(全域)會先嘗試把 x 轉成 number 再判斷是不是 NaN,例如 isNaN('hello') 會是 true(因為 'hello' 轉數字變成 NaN)。Number.isNaN(x) 不會做強制轉型,只有在「型別是 number 且值是 NaN」時才回傳 true。用 parseInt 或 Number() 把值轉成 number,接著用 Number.isNaN(n) 檢查會比較直覺、也比較安全。參考:NaN 參考:isNaN() 參考:Number.isNaN()陣列方法與 for loop陣列方法(map、filter、reduce 等)在處理「整個陣列的轉換」時,語意通常比裸 for 迴圈更清楚,可讀性較好。但在需要 break / continue、或對性能非常敏感的情況,傳統 for / for...of 仍然是很好的選擇。可以把原則調整為:優先考慮用陣列方法讓 intent 更清楚。如果遇到需要細緻控制流程或性能要求,再回到 for。參考:陣列與陣列方法readlinereadline.createInterface({ input, output }) 裡:input 通常綁定 process.stdin,代表從終端機讀入的資料。output 通常綁定 process.stdout,代表在終端機顯示的提示文字。console.log 單純寫到 stdout,與 readline 是否存在無直接關係。rl.close() 用來關閉 Interface,釋放對 input / output 的控制,觸發 close 事件:建議在一個互動流程結束後就呼叫 rl.close(),避免忘記關閉導致後續程式無法正常使用 readline 或卡在等待輸入。參考:Readline | Node.js模組化程式碼模組 1:共用流程抽到 /util將多支題目會重複用到的流程抽成 util 函式,例如:數字輸入驗證共同的格式化邏輯要給其他檔案使用的函式,用 export 暴露出來,這些對外暴露的函式可以視為這個模組的 public API。「最小 TDD」原則下,可以這樣安排測試重點:優先完整測 public API(例如 rotate90、calString 或 /util 中 export 出來的函式),涵蓋正常情境、邊界情境與錯誤輸入。檔案內部只給自己用的 helper 函式,優先透過 public API 間接被測到,不一定都要另外 export 出來寫獨立測試。如果某個 helper 邏輯很複雜、難以透過 public API 測試,就考慮把它抽到 /util 作為獨立模組的 public API,並為它寫測試。這樣可以在「練習 TDD」與「維持模組邊界乾淨」之間取得平衡:測試專注於對外承諾的行為,內部實作則依照需要逐步重構。參考:JavaScript modules模組 2:輸入檢查(validation)建立專門負責輸入驗證的函式或模組,例如:validatePositiveInteger(n):檢查是否為大於 0 的整數,不合法時丟錯。好處:domain 函式邏輯更純粹、只關注「題目核心運算」。驗證邏輯可以共用,測試也集中。模組 3:readline I/O 啟動 / 關閉可以把「建立 readline、詢問問題、關閉」封裝成單獨模組,例如:createCli() 回傳一個 ask(question) 的 async 函式,內部自行處理 rl 的建立與關閉。這樣每一題 CLI 程式都可以只專注在:題目運算如何把結果印出而 I/O 細節則集中管理在這個模組中。 參考:readline 模組從「窮舉案例」到「規格層級」的測試目前對多項式 function calString(n) 的測試是類似「窮舉案例」的寫法,例如:// calString(n) 範例: // calString(3) => "1+2-3=0" // calString(6) => "1+2-3+4-5+6=5" test.each([ [6, "1+2-3+4-5+6=5"], [10, "1+2-3+4-5+6-7+8-9+10=7"], [1, "1=1"], [3, "1+2-3=0"], ])('輸入 %i 應印出正確算式與結果', (n, expected) => { expect(calString(n)).toBe(expected); }); 這種做法的特徵是:一次檢查「整串輸出」是否完全等於預期字串。想增加信心時,自然的做法是「多塞幾個 n,多窮舉幾組 input/output」。這已經能有效驗證「小範圍內的行為」,但還可以再往下拆成「規格層級」的測試,分別檢查不同規則。規格層級測試:拆成三種規則來驗證以一個簡化版函式 sumString(n) 為例:// 回傳 1 + 2 + ... + n 的字串與總和,例如: // sumString(3) => "1+2+3=6" function sumString (n) { /* ... */ } 1. 規則 A:數值結果要正確不在乎左邊長什麼樣,單純檢查「= 右邊的數值」是否正確。test.each([ [1, 1], // 1 [3, 6], // 1 + 2 + 3 [5, 15], // 1 + 2 + 3 + 4 + 5 ])('輸入 %i 時,總和要正確', (n, expectedSum) => { const result = sumString(n); // "1+2+3=6" const [, sumStr] = result.split('='); expect(Number(sumStr)).toBe(expectedSum); }); 對應回 calString(n),就是只抽出 = 右邊的 sum 來比對,而不是整串字串。2. 規則 B:字串格式要符合規定專門檢查算式格式是否符合規則,例如:第一個數不帶 + 或 -。後面的正數都要帶 +。整體只包含數字、+、-。test('格式:第一個數不帶 +,其餘正數帶 +,結尾有 =sum', () => { const result = sumString(5); // "1+2+3+4+5=15" const [expr] = result.split('='); // "1+2+3+4+5" // 利用正則把每一段數字(含前面的符號)切出來 const tokens = expr.match(/[+-]?\d+/g); // ["1", "+2", "+3", "+4", "+5"] // 規則 B1:第一個 token 不可以以 '+' 開頭 expect(tokens.startsWith('+')).toBe(false); // 規則 B2:後面每個 token 如果不是負數,就必須以 '+' 開頭 for (const token of tokens.slice(1)) { if (!token.startsWith('-')) { expect(token.startsWith('+')).toBe(true); } } }); 對應回 calString(n),就可以檢查:第一項沒有符號。後面為正的都是 +數字,為負的是 -數字。3. 規則 C:輸入驗證(非法輸入要丟錯)專門檢查輸入是否合法,而不是算式正不正確:test.each([[0], [-1], [1.5], ['3']])('輸入非正整數 %p 應丟錯', (value) => { expect(() => sumString(value)).toThrow(); }); 對應回 calString(n),就是補上「n 不是正整數時要丟錯」的測試,而不是再多列幾個正常的 n。兩種測試思路的差異類窮舉測試(現在已經做的)多組 n → 比對「整串輸出」是否完全等於預期。好處:直覺、一次看「輸入→輸出」是否正確。規格層級測試(可以再補的)先把需求拆成多條規則(數值結果、字串格式、輸入合法性),各自寫測試。好處:哪條規則壞了,測試訊息更清楚;未來改格式或內部實作時,較不容易全掛。放在現在的多項式練習上,就是:除了「針對多個 n 比對整串算式字串」,還可以:拆出「結果數值規則」來測 sum。拆出「格式規則」來測字串長相。拆出「輸入規則」來測錯誤處理。讓測試從「多案例比對」進一步升級成「行為規格的集合」。 ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates