線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
<100 subscribers
目前覺得最難的是:先用自然語言講清楚「這個函式要做什麼」之後,再把它拆成「可以實作的步驟」,最後才寫出對應的程式碼。
這一層「從需求句子 → 步驟化描述」就是 TDD 對我來說最卡的一關。
下面把這次重構 CLI 驗證流程時的幾個關鍵觀念,整理成「心法+概念程式碼」的形式。
以「組合多個驗證器」這個問題為例,需求可以用一句話講完:
給我一串 validator 函式,照順序執行,每一個都吃前一個的結果,
任一個丟錯就中止,最後回最後那一步的輸出。
把這句話轉成比較「程式化」的描述:
輸入:validators 陣列,每個長得像 f(value) -> { value } | throws。
回傳:一個新函式 g(input)。
行為:
設 current = input。
對每個 validator:
執行 result = validator(current)。
更新 current = result.value。
全部跑完後回 { value: current }。
任一 validator 丟錯 → 不繼續後面,錯誤直接往外拋。
用概念化的程式碼來表示,就是:
// composeValidators 概念版
composeValidators([f1, f2, f3]) 回傳 g
g(x):
current = x
for f in [f1, f2, f3]:
result = f(current) // { value: ... } | throws
current = result.value
回傳 { value: current } // 最後一個 value 包成物件
TDD 的第一步,就是先把這樣的「語言規格」寫進測試描述裡,再寫一個剛好讓測試變綠的實作。
以 composeValidators 的最小測試來看,可以先只描述「一條快樂路徑」:
給兩個小小的 validator:
v1(x) => { value: x + 1 }
v2(x) => { value: x * 2 }
組成 pipeline 後,希望:
composed(3) === { value: 8 } // (3 + 1) * 2
對應到概念程式碼就是:
const composed = composeValidators([v1, v2])
const result = composed(3)
// 預期 result 是 { value: 8 }
只要讓這個測試過,最小實作大概長這樣就夠了:
composeValidators(validators):
回傳函式 g(value):
current = value
對每個 validate in validators:
result = validate(current) // { value: ... }
current = result.value
回傳 { value: current }
這時候還不急著處理錯誤、中止、各種邊界,只要讓「正常情境」走得通即可。
下一步再加一個測試,專門描述「如果中途有驗證失敗」:
排三個 validator:v1, v2, v3。
其中 v2 會丟錯:
v1(x): 紀錄 'v1',回 { value: x + 1 }
v2(x): 紀錄 'v2',丟 Error('v2 error')
v3(x): 紀錄 'v3',回 { value: x * 2 }
期待行為:
composed(3) 會丟 'v2 error'
calls === ['v1', 'v2'] // v3 不應該被叫到
這裡不用在 composeValidators 裡加任何 try/catch,相反地:
只要不要吃掉錯誤:讓 v2 丟的錯照原樣往外跑。
for 迴圈自然就會中止,不會跑到 v3。
也就是說,這個測試只是確認:「只要不在 compose 裡處理錯誤,中止的行為就自然而然會出現」,不需要特別加額外邏輯。
當 composeValidators 的基本行為穩定之後,就可以寫一個貼近實際 CLI 的測試:
組出一條真正會在 CLI 用到的驗證 pipeline,例如「大於等於 4 的偶數」:
const validateEvenNum = composeValidators([
ensureIntegerString, // ' 6 ' → { value: '6' }
toInt, // '6' → { value: 6 }
(n) => isEvenNum(n, 4), // 6 → { value: 6 } 或丟錯
])
驗證三件事:
validateEvenNum(' 6 ') → { value: 6 }
validateEvenNum('abc') 丟錯
validateEvenNum('3') 丟錯
這個測試的目的不是「再檢查一次 validators 行為」,而是確認:
composeValidators 的介面設計(吃純值 validator、回 { value })能支撐實際 CLI 的使用情境。
真正 validators.js 裡的實作跟 compose 的設計不打架,之後 refactor validators,也有一個小整合測試可以「兜住」。
在「問問題+驗證,錯了要重問」這個場景裡,本質上的控制流程其實非常簡單:
// 原先 while loop 的邏輯(同步版腦中模型)
while (true) {
詢問;
if (成功) return 值;
else 印錯再來;
}
只是 CLI 世界裡的「詢問」是非同步 (rl.question),不能真的寫成同步 while,就會演變出不同寫法。
先把「問一次」抽成 questionOnce(回 Promise),再在外層寫一個 loop 函式:
const loop = () => {
問一次()
.then(驗證成功就回值)
.catch(印錯後 return loop()); // 下一圈
};
return loop(); // 啟動第一圈
語意對照:
then 裡:對應「if (成功) return 值」。
catch 裡的 return loop():對應「else 印錯再來」。
外層的 return loop():對應「從第一次提問開始」。
如果已經把 I/O 抽出去(有了 questionOnce),在一個職責單一的小函式裡,用 async/await 寫回「看起來像同步 while」的東西,會更好讀:
async function askWithValidator(詢問一次, 驗證器) {
while (true) {
const raw = await 詢問一次(); // 問一次(Promise 版 readline)
try {
const value = 驗證器(raw); // 同步驗證:成功回值,失敗 throw
return value; // 成功 → 結束整個函式(等價 resolve 這個值)
} catch (error) {
console.log('驗證器錯誤訊息:', error.message);
// 失敗 → 不 return,while 進入下一圈,再 await 詢問一次()
}
}
}
可以直接對應回最一開始那個同步 while 的腦中模型:
while (true):一直問。
await 詢問一次():每圈的「詢問」。
try 裡 return value:成功就跳出整個流程。
catch 裡印錯不 return:失敗就回到 while 頭再來一次。
今天在練這些模組時,測試裡用到的替身都是「手刻的小型假物件/函式」,還沒有用到 Vitest 的 vi.fn / vi.spyOn。
依賴的介面很小、行為很簡單:
像 rl.question 只需一個方法。
只需要「回假資料」,不太關心呼叫紀錄:
例如 fake readline:
const calls = [];
const fakeRl = {
question: (prompt, cb) => {
calls.push(prompt);
cb('user input');
},
};
或控制輸入序列:
const userInputs = ['abc', '6'];
let callIndex = 0;
const rl = {
question: (prompt, cb) => {
const userInput = userInputs[callIndex];
callIndex += 1;
cb(userInput);
},
};
好處:
型別與介面一眼就懂。
不用先學 mock API,把心力放在「函式規格與流程」。
假物件很小,不會變成又大又難維護的 Fake 系統。
當開始需要注意「怎麼被叫」而不只是「回什麼」時:
需要檢查有沒有被叫、被叫幾次、參數是什麼。
例如:
runCliSession 要確認 createRl 被叫一次、rl.close 不論成功或失敗都會被呼叫。
runCliQuestions 要確認 askWithValidator 有依序被呼叫過每個題目。
當某個 fake / stub 開始在多個測試中複用、越寫越肥時:
可以考慮用 vi.fn 做一個工廠函式,或用 vi.spyOn 在現有模組上掛 spy,減少手刻重複。
簡單的決策流程可以是:
這個依賴介面小不小?
小 → 先手刻。
只要假資料、還是要檢查呼叫紀錄?
只要假資料 → 手刻。
要檢查呼叫方式 → 開始用 vi.fn / vi.spyOn。
這個假物件是不是越寫越肥?
是 → 重構為正式模組,或用 vi API 瘦身。
TDD 的「最小第一步」,就是先用自然語言寫清楚:「這個函式吃什麼、要做什麼、什麼時候丟錯」。
再把那句話翻成像「小演算法」一樣的步驟化描述,最後才寫程式。
控制流程可以從 while 的腦中模型開始想,再決定適合用 Promise 鏈還是 async/await。
重構時,先把「職責」拆開(I/O / 控制流程 / 驗證邏輯),等每一層責任清楚了,while 或遞迴就只是風格選擇。
練 test double 時,先手刻最小的 stub/fake,把流程跑通;需要檢查呼叫細節或假物件變肥時,再引入 vi.fn / vi.spyOn,一步一步加強。
目前覺得最難的是:先用自然語言講清楚「這個函式要做什麼」之後,再把它拆成「可以實作的步驟」,最後才寫出對應的程式碼。
這一層「從需求句子 → 步驟化描述」就是 TDD 對我來說最卡的一關。
下面把這次重構 CLI 驗證流程時的幾個關鍵觀念,整理成「心法+概念程式碼」的形式。
以「組合多個驗證器」這個問題為例,需求可以用一句話講完:
給我一串 validator 函式,照順序執行,每一個都吃前一個的結果,
任一個丟錯就中止,最後回最後那一步的輸出。
把這句話轉成比較「程式化」的描述:
輸入:validators 陣列,每個長得像 f(value) -> { value } | throws。
回傳:一個新函式 g(input)。
行為:
設 current = input。
對每個 validator:
執行 result = validator(current)。
更新 current = result.value。
全部跑完後回 { value: current }。
任一 validator 丟錯 → 不繼續後面,錯誤直接往外拋。
用概念化的程式碼來表示,就是:
// composeValidators 概念版
composeValidators([f1, f2, f3]) 回傳 g
g(x):
current = x
for f in [f1, f2, f3]:
result = f(current) // { value: ... } | throws
current = result.value
回傳 { value: current } // 最後一個 value 包成物件
TDD 的第一步,就是先把這樣的「語言規格」寫進測試描述裡,再寫一個剛好讓測試變綠的實作。
以 composeValidators 的最小測試來看,可以先只描述「一條快樂路徑」:
給兩個小小的 validator:
v1(x) => { value: x + 1 }
v2(x) => { value: x * 2 }
組成 pipeline 後,希望:
composed(3) === { value: 8 } // (3 + 1) * 2
對應到概念程式碼就是:
const composed = composeValidators([v1, v2])
const result = composed(3)
// 預期 result 是 { value: 8 }
只要讓這個測試過,最小實作大概長這樣就夠了:
composeValidators(validators):
回傳函式 g(value):
current = value
對每個 validate in validators:
result = validate(current) // { value: ... }
current = result.value
回傳 { value: current }
這時候還不急著處理錯誤、中止、各種邊界,只要讓「正常情境」走得通即可。
下一步再加一個測試,專門描述「如果中途有驗證失敗」:
排三個 validator:v1, v2, v3。
其中 v2 會丟錯:
v1(x): 紀錄 'v1',回 { value: x + 1 }
v2(x): 紀錄 'v2',丟 Error('v2 error')
v3(x): 紀錄 'v3',回 { value: x * 2 }
期待行為:
composed(3) 會丟 'v2 error'
calls === ['v1', 'v2'] // v3 不應該被叫到
這裡不用在 composeValidators 裡加任何 try/catch,相反地:
只要不要吃掉錯誤:讓 v2 丟的錯照原樣往外跑。
for 迴圈自然就會中止,不會跑到 v3。
也就是說,這個測試只是確認:「只要不在 compose 裡處理錯誤,中止的行為就自然而然會出現」,不需要特別加額外邏輯。
當 composeValidators 的基本行為穩定之後,就可以寫一個貼近實際 CLI 的測試:
組出一條真正會在 CLI 用到的驗證 pipeline,例如「大於等於 4 的偶數」:
const validateEvenNum = composeValidators([
ensureIntegerString, // ' 6 ' → { value: '6' }
toInt, // '6' → { value: 6 }
(n) => isEvenNum(n, 4), // 6 → { value: 6 } 或丟錯
])
驗證三件事:
validateEvenNum(' 6 ') → { value: 6 }
validateEvenNum('abc') 丟錯
validateEvenNum('3') 丟錯
這個測試的目的不是「再檢查一次 validators 行為」,而是確認:
composeValidators 的介面設計(吃純值 validator、回 { value })能支撐實際 CLI 的使用情境。
真正 validators.js 裡的實作跟 compose 的設計不打架,之後 refactor validators,也有一個小整合測試可以「兜住」。
在「問問題+驗證,錯了要重問」這個場景裡,本質上的控制流程其實非常簡單:
// 原先 while loop 的邏輯(同步版腦中模型)
while (true) {
詢問;
if (成功) return 值;
else 印錯再來;
}
只是 CLI 世界裡的「詢問」是非同步 (rl.question),不能真的寫成同步 while,就會演變出不同寫法。
先把「問一次」抽成 questionOnce(回 Promise),再在外層寫一個 loop 函式:
const loop = () => {
問一次()
.then(驗證成功就回值)
.catch(印錯後 return loop()); // 下一圈
};
return loop(); // 啟動第一圈
語意對照:
then 裡:對應「if (成功) return 值」。
catch 裡的 return loop():對應「else 印錯再來」。
外層的 return loop():對應「從第一次提問開始」。
如果已經把 I/O 抽出去(有了 questionOnce),在一個職責單一的小函式裡,用 async/await 寫回「看起來像同步 while」的東西,會更好讀:
async function askWithValidator(詢問一次, 驗證器) {
while (true) {
const raw = await 詢問一次(); // 問一次(Promise 版 readline)
try {
const value = 驗證器(raw); // 同步驗證:成功回值,失敗 throw
return value; // 成功 → 結束整個函式(等價 resolve 這個值)
} catch (error) {
console.log('驗證器錯誤訊息:', error.message);
// 失敗 → 不 return,while 進入下一圈,再 await 詢問一次()
}
}
}
可以直接對應回最一開始那個同步 while 的腦中模型:
while (true):一直問。
await 詢問一次():每圈的「詢問」。
try 裡 return value:成功就跳出整個流程。
catch 裡印錯不 return:失敗就回到 while 頭再來一次。
今天在練這些模組時,測試裡用到的替身都是「手刻的小型假物件/函式」,還沒有用到 Vitest 的 vi.fn / vi.spyOn。
依賴的介面很小、行為很簡單:
像 rl.question 只需一個方法。
只需要「回假資料」,不太關心呼叫紀錄:
例如 fake readline:
const calls = [];
const fakeRl = {
question: (prompt, cb) => {
calls.push(prompt);
cb('user input');
},
};
或控制輸入序列:
const userInputs = ['abc', '6'];
let callIndex = 0;
const rl = {
question: (prompt, cb) => {
const userInput = userInputs[callIndex];
callIndex += 1;
cb(userInput);
},
};
好處:
型別與介面一眼就懂。
不用先學 mock API,把心力放在「函式規格與流程」。
假物件很小,不會變成又大又難維護的 Fake 系統。
當開始需要注意「怎麼被叫」而不只是「回什麼」時:
需要檢查有沒有被叫、被叫幾次、參數是什麼。
例如:
runCliSession 要確認 createRl 被叫一次、rl.close 不論成功或失敗都會被呼叫。
runCliQuestions 要確認 askWithValidator 有依序被呼叫過每個題目。
當某個 fake / stub 開始在多個測試中複用、越寫越肥時:
可以考慮用 vi.fn 做一個工廠函式,或用 vi.spyOn 在現有模組上掛 spy,減少手刻重複。
簡單的決策流程可以是:
這個依賴介面小不小?
小 → 先手刻。
只要假資料、還是要檢查呼叫紀錄?
只要假資料 → 手刻。
要檢查呼叫方式 → 開始用 vi.fn / vi.spyOn。
這個假物件是不是越寫越肥?
是 → 重構為正式模組,或用 vi API 瘦身。
TDD 的「最小第一步」,就是先用自然語言寫清楚:「這個函式吃什麼、要做什麼、什麼時候丟錯」。
再把那句話翻成像「小演算法」一樣的步驟化描述,最後才寫程式。
控制流程可以從 while 的腦中模型開始想,再決定適合用 Promise 鏈還是 async/await。
重構時,先把「職責」拆開(I/O / 控制流程 / 驗證邏輯),等每一層責任清楚了,while 或遞迴就只是風格選擇。
練 test double 時,先手刻最小的 stub/fake,把流程跑通;需要檢查呼叫細節或假物件變肥時,再引入 vi.fn / vi.spyOn,一步一步加強。
線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
Share Dialog
Share Dialog
No comments yet