線上課程觀課進度管理小工具開發日誌
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
線上課程觀課進度管理小工具開發日誌
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」。
在單元測試語境下,「單元」指的是系統內部的一個「工作單元」或「用例(use case)」,不一定只是一個函式。它可以是從一次調用開始,到產生某個明確結果為止,這段過程裡所有一起合作的程式碼。
**工作單元(unit of work)**可以是一個函式,也可以是多個函式組成的流程,只要是「從一個調用開始,到產生結果為止」的一整段邏輯。
這段流程從 進入點(entry point) 開始,直到一個或多個 退出點(exit point) 結束。
在寫測試時:
進入點就是「測試程式呼叫哪個 API」:例如呼叫一個函式、送一個 HTTP 請求、操作一個公開 method。
每個退出點代表「一種行為或需求」,通常會為不同的退出方式寫對應的測試案例。
典型的退出點大致可以分成三類,對應不同測試手法:
回傳值
最直覺的情況,測試會對函式的回傳值做斷言。
適合純計算、純轉換類函式(沒有副作用)。
狀態改變
函式不一定回傳有用的東西,但會改變某個狀態(物件屬性、全域狀態、DOM、store 等)。
測試會在呼叫後檢查狀態是否符合預期。
呼叫第三方/依賴項目
例如:呼叫資料庫、發送 HTTP request、寫檔案。
單元測試通常不直接真的去打外部系統,而是用 stub / mock(test doubles)檢查「有沒有用正確參數呼叫依賴」。
這三種退出點也可以同時存在:例如某個函式既會回傳值,又會改變狀態,還會呼叫外部服務,測試時就要清楚知道要驗什麼、哪些要用 stub 取代。
書裡整理了一些特徵,這些其實對所有自動化測試都適用:
容易閱讀、容易編寫,一看就知道作者想驗證什麼。
自動化,按一下就能跑完。
結果一致,不會「有時過、有時不過」。
有用:失敗時能指出有意義的資訊,幫助找到問題。
可以在任何地方、任何人一鍵執行,不綁定作者機器上的特殊設定。
常見的單元測試額外特性:
執行速度快。
能完全控制受測程式碼(含 stub 掉外部依賴)。
測試之間彼此獨立,不互相影響。
在記憶體內運行,不依賴檔案系統、網路、資料庫等 I/O。
在合理狀況下盡可能保持同步、線性執行,降低平行執行緒/非同步帶來的不穩定因素。
在 JavaScript 環境裡,這也呼應了為什麼 async/await 這種語法糖這麼重要:它讓非同步流程可以用「看起來像同步」的方式描述,測試時也能寫出比較線性的斷言流程。[1]
即便底層是非同步(例如 Promise、計時器、HTTP request),測試可以透過幾種方式保持「語意上的同步」:
使用 async/await 把非同步流程寫成「直式」敘述。
若沒有語法糖(例如在 callback 風格 API 中),可以在測試裡明確等待非同步完成:
例如傳入 done callback、或在測試框架提供的 API 裡 await 某個 Promise。
重要的是:測試本身要「知道什麼時候可以斷言」,而不是隨機等一段時間。
這樣一來,即使系統內部充滿非同步,測試的進入點與退出點依然是清楚、可控的。
接下來的案例,是一個「統計每種器材數量」的小需求,很適合作為「物件當 Map 用」與 Object.values() 的練習。[2][1]
輸入:
[
{ name: 'Kettlebell', spec: '6 kg', weight: 6 },
{ name: 'Kettlebell', spec: '6 kg', weight: 6 }, // 重複
{ name: 'Dumbbell', spec: '10 kg', weight: 10 },
]
輸出:
[
{ name: 'Kettlebell', spec: '6 kg', weight: 6, count: 2 },
{ name: 'Dumbbell', spec: '10 kg', weight: 10, count: 1 },
]
核心邏輯:掃過一遍清單,用「名稱 + 規格」當 key 把同一種器材累積起來,最後把結果轉回陣列。
const equipmentsReport = [];
equipmentsList.forEach((item) => {
const key = `${item.name}-${item.spec}`;
const existing = equipmentsReport.find((report) => (
`${report.name}-${report.spec}` === key
));
if (existing) {
existing.count += 1;
} else {
equipmentsReport.push({ ...item, count: 1 });
}
});
外層 forEach 跑 n 次,每次裡面 find 平均要掃 n/2 個元素。
整體時間複雜度約 O(n²),資料一多就會明顯變慢。
const countMap = {};
equipmentsList.forEach((item) => {
const key = `${item.name}-${item.spec}`;
if (countMap[key]) {
countMap[key].count += 1;
} else {
countMap[key] = { ...item, count: 1 };
}
});
const summary = Object.values(countMap);
countMap[key] 查找是物件屬性存取,平均視為 O(1)。
整體流程是單一遍歷,時間複雜度 O(n)。
最後用 Object.values(countMap) 把 Map 結果轉回陣列。[1]
選擇理由:
效能:從 O(n²) 降到 O(n)。
語意:用「查表」取代「每次搜尋整個陣列」,閱讀起來更貼近問題本質。
實作:Object.values() 是標準 API,支援度很好。[1]
回傳「物件自己的、可列舉、字串 key 的屬性值」陣列。
不會包含原型鏈上的屬性。
不會包含 Symbol key。
const obj = { a: 1, b: 2 };
Object.values(obj); // [1, 2]
這就是前面 countMap 統計完後,用來取得結果陣列的關鍵 API。[3][1]
回傳「[key, value]」二元陣列組成的陣列。
const obj = { a: 1, b: 2 };
Object.entries(obj); // [['a', 1], ['b', 2]]
常用在需要同時拿 key 與 value 的場合,例如把物件轉成 Map 或是用解構搭配 for...of 做迴圈。[4][2]
統計時的 key 通常是組合而來,例如 Kettlebell-6 kg,這個 key 裡面有空白與 dash,不可能用點語法:
const obj = {};
const key = 'Kettlebell-6 kg';
// ✅ 正確寫法
obj[key] = { name: 'Kettlebell', spec: '6 kg', weight: 6 };
// ❌ 錯誤:點語法只能用合法識別字
// obj.Kettlebell-6 kg = ... // 這會直接語法錯誤
這也是「物件當 Map」模式中,方括號語法不可或缺的原因。
實務上有三種常見寫法,各有適用情境。
if (countMap[key]) {
countMap[key].count += 1;
} else {
countMap[key] = { ...item, count: 1 };
}
適用前提:
確定 value 不會是 0、false、'' 等 falsy 值。
物件是自己建立的,不是外部輸入。
在「統計次數」這種情境,count 一開始是 1 之後只會變大,所以不會是 falsy,非常適合用這種寫法。
if (Object.hasOwn(countMap, key)) {
// ...
}
ES2022 新增,語法精簡,專門用來取代 obj.hasOwnProperty(key)。
使用前要確認執行環境(現代 Node / 瀏覽器基本沒問題)。[5]
if (Object.prototype.hasOwnProperty.call(countMap, key)) {
// ...
}
相容性最好、最安全的老派寫法。
主要是避免兩種情況:
物件自己定義了 hasOwnProperty,把原本的方法蓋掉。
物件是 Object.create(null) 建出來的,根本沒有原型鏈。[6]
ESLint 的 no-prototype-builtins 規則就是在提醒:不要直接用 obj.hasOwnProperty(key),改用上面兩種其中之一。[7][8][9]
/**
* 統計器材清單中每種器材的數量
* @param {Array<Object>} equipmentsList - 器材陣列
* @returns {Array<Object>} 統計後的陣列,格式:[{ name, spec, weight, count }, ...]
*/
export function countEquipments(equipmentsList) {
const countMap = {};
equipmentsList.forEach((item) => {
const key = `${item.name}-${item.spec}`;
if (countMap[key]) {
countMap[key].count += 1;
} else {
countMap[key] = {
name: item.name,
spec: item.spec,
weight: item.weight,
count: 1,
};
}
});
return Object.values(countMap);
}
這個模式可以重複用在各種「統計每種東西出現幾次」的場景,例如:
統計購物車裡每種商品數量。
統計 log 檔中每種錯誤代碼的出現次數。
統計 API 回傳資料中,每個城市出現幾次。
關鍵元素只有三個:
用 Object 當 Map 儲存中間結果。
用「某種唯一 key」把同一種東西歸在一起。
最後用 Object.values() 把 Map 轉回陣列。[1]
在單元測試裡,先想清楚工作單元的進入點和退出點,再設計測試案例:不同退出點通常代表不同需求。
好的單元測試應該可讀、可重複、可自動,並且盡量在記憶體內同步運行,不依賴外部 I/O。
JavaScript 的 async/await 是把非同步邏輯以同步形式表達的語法糖,有助於讓測試流程更線性。
在「統計」這種問題上,Object 當 Map + Object.values 是非常實用的模式,時間複雜度從 O(n²) 降到 O(n)。[2][1]
檢查物件屬性存在時,要根據情境選擇:
自己控制的資料 + 值不會是 falsy:if (obj[key]) 就夠用。
嚴謹、泛用:Object.hasOwn(obj, key) 或 Object.prototype.hasOwnProperty.call(obj, key),並搭配 ESLint 規則避免陷阱。[9][7]
如果之後想把這段統計邏輯加上單元測試,下一步就可以練習:為 countEquipments 設計進入點(呼叫函式)、退出點(回傳值)與幾組不同情境的 test cases。
在單元測試語境下,「單元」指的是系統內部的一個「工作單元」或「用例(use case)」,不一定只是一個函式。它可以是從一次調用開始,到產生某個明確結果為止,這段過程裡所有一起合作的程式碼。
**工作單元(unit of work)**可以是一個函式,也可以是多個函式組成的流程,只要是「從一個調用開始,到產生結果為止」的一整段邏輯。
這段流程從 進入點(entry point) 開始,直到一個或多個 退出點(exit point) 結束。
在寫測試時:
進入點就是「測試程式呼叫哪個 API」:例如呼叫一個函式、送一個 HTTP 請求、操作一個公開 method。
每個退出點代表「一種行為或需求」,通常會為不同的退出方式寫對應的測試案例。
典型的退出點大致可以分成三類,對應不同測試手法:
回傳值
最直覺的情況,測試會對函式的回傳值做斷言。
適合純計算、純轉換類函式(沒有副作用)。
狀態改變
函式不一定回傳有用的東西,但會改變某個狀態(物件屬性、全域狀態、DOM、store 等)。
測試會在呼叫後檢查狀態是否符合預期。
呼叫第三方/依賴項目
例如:呼叫資料庫、發送 HTTP request、寫檔案。
單元測試通常不直接真的去打外部系統,而是用 stub / mock(test doubles)檢查「有沒有用正確參數呼叫依賴」。
這三種退出點也可以同時存在:例如某個函式既會回傳值,又會改變狀態,還會呼叫外部服務,測試時就要清楚知道要驗什麼、哪些要用 stub 取代。
書裡整理了一些特徵,這些其實對所有自動化測試都適用:
容易閱讀、容易編寫,一看就知道作者想驗證什麼。
自動化,按一下就能跑完。
結果一致,不會「有時過、有時不過」。
有用:失敗時能指出有意義的資訊,幫助找到問題。
可以在任何地方、任何人一鍵執行,不綁定作者機器上的特殊設定。
常見的單元測試額外特性:
執行速度快。
能完全控制受測程式碼(含 stub 掉外部依賴)。
測試之間彼此獨立,不互相影響。
在記憶體內運行,不依賴檔案系統、網路、資料庫等 I/O。
在合理狀況下盡可能保持同步、線性執行,降低平行執行緒/非同步帶來的不穩定因素。
在 JavaScript 環境裡,這也呼應了為什麼 async/await 這種語法糖這麼重要:它讓非同步流程可以用「看起來像同步」的方式描述,測試時也能寫出比較線性的斷言流程。[1]
即便底層是非同步(例如 Promise、計時器、HTTP request),測試可以透過幾種方式保持「語意上的同步」:
使用 async/await 把非同步流程寫成「直式」敘述。
若沒有語法糖(例如在 callback 風格 API 中),可以在測試裡明確等待非同步完成:
例如傳入 done callback、或在測試框架提供的 API 裡 await 某個 Promise。
重要的是:測試本身要「知道什麼時候可以斷言」,而不是隨機等一段時間。
這樣一來,即使系統內部充滿非同步,測試的進入點與退出點依然是清楚、可控的。
接下來的案例,是一個「統計每種器材數量」的小需求,很適合作為「物件當 Map 用」與 Object.values() 的練習。[2][1]
輸入:
[
{ name: 'Kettlebell', spec: '6 kg', weight: 6 },
{ name: 'Kettlebell', spec: '6 kg', weight: 6 }, // 重複
{ name: 'Dumbbell', spec: '10 kg', weight: 10 },
]
輸出:
[
{ name: 'Kettlebell', spec: '6 kg', weight: 6, count: 2 },
{ name: 'Dumbbell', spec: '10 kg', weight: 10, count: 1 },
]
核心邏輯:掃過一遍清單,用「名稱 + 規格」當 key 把同一種器材累積起來,最後把結果轉回陣列。
const equipmentsReport = [];
equipmentsList.forEach((item) => {
const key = `${item.name}-${item.spec}`;
const existing = equipmentsReport.find((report) => (
`${report.name}-${report.spec}` === key
));
if (existing) {
existing.count += 1;
} else {
equipmentsReport.push({ ...item, count: 1 });
}
});
外層 forEach 跑 n 次,每次裡面 find 平均要掃 n/2 個元素。
整體時間複雜度約 O(n²),資料一多就會明顯變慢。
const countMap = {};
equipmentsList.forEach((item) => {
const key = `${item.name}-${item.spec}`;
if (countMap[key]) {
countMap[key].count += 1;
} else {
countMap[key] = { ...item, count: 1 };
}
});
const summary = Object.values(countMap);
countMap[key] 查找是物件屬性存取,平均視為 O(1)。
整體流程是單一遍歷,時間複雜度 O(n)。
最後用 Object.values(countMap) 把 Map 結果轉回陣列。[1]
選擇理由:
效能:從 O(n²) 降到 O(n)。
語意:用「查表」取代「每次搜尋整個陣列」,閱讀起來更貼近問題本質。
實作:Object.values() 是標準 API,支援度很好。[1]
回傳「物件自己的、可列舉、字串 key 的屬性值」陣列。
不會包含原型鏈上的屬性。
不會包含 Symbol key。
const obj = { a: 1, b: 2 };
Object.values(obj); // [1, 2]
這就是前面 countMap 統計完後,用來取得結果陣列的關鍵 API。[3][1]
回傳「[key, value]」二元陣列組成的陣列。
const obj = { a: 1, b: 2 };
Object.entries(obj); // [['a', 1], ['b', 2]]
常用在需要同時拿 key 與 value 的場合,例如把物件轉成 Map 或是用解構搭配 for...of 做迴圈。[4][2]
統計時的 key 通常是組合而來,例如 Kettlebell-6 kg,這個 key 裡面有空白與 dash,不可能用點語法:
const obj = {};
const key = 'Kettlebell-6 kg';
// ✅ 正確寫法
obj[key] = { name: 'Kettlebell', spec: '6 kg', weight: 6 };
// ❌ 錯誤:點語法只能用合法識別字
// obj.Kettlebell-6 kg = ... // 這會直接語法錯誤
這也是「物件當 Map」模式中,方括號語法不可或缺的原因。
實務上有三種常見寫法,各有適用情境。
if (countMap[key]) {
countMap[key].count += 1;
} else {
countMap[key] = { ...item, count: 1 };
}
適用前提:
確定 value 不會是 0、false、'' 等 falsy 值。
物件是自己建立的,不是外部輸入。
在「統計次數」這種情境,count 一開始是 1 之後只會變大,所以不會是 falsy,非常適合用這種寫法。
if (Object.hasOwn(countMap, key)) {
// ...
}
ES2022 新增,語法精簡,專門用來取代 obj.hasOwnProperty(key)。
使用前要確認執行環境(現代 Node / 瀏覽器基本沒問題)。[5]
if (Object.prototype.hasOwnProperty.call(countMap, key)) {
// ...
}
相容性最好、最安全的老派寫法。
主要是避免兩種情況:
物件自己定義了 hasOwnProperty,把原本的方法蓋掉。
物件是 Object.create(null) 建出來的,根本沒有原型鏈。[6]
ESLint 的 no-prototype-builtins 規則就是在提醒:不要直接用 obj.hasOwnProperty(key),改用上面兩種其中之一。[7][8][9]
/**
* 統計器材清單中每種器材的數量
* @param {Array<Object>} equipmentsList - 器材陣列
* @returns {Array<Object>} 統計後的陣列,格式:[{ name, spec, weight, count }, ...]
*/
export function countEquipments(equipmentsList) {
const countMap = {};
equipmentsList.forEach((item) => {
const key = `${item.name}-${item.spec}`;
if (countMap[key]) {
countMap[key].count += 1;
} else {
countMap[key] = {
name: item.name,
spec: item.spec,
weight: item.weight,
count: 1,
};
}
});
return Object.values(countMap);
}
這個模式可以重複用在各種「統計每種東西出現幾次」的場景,例如:
統計購物車裡每種商品數量。
統計 log 檔中每種錯誤代碼的出現次數。
統計 API 回傳資料中,每個城市出現幾次。
關鍵元素只有三個:
用 Object 當 Map 儲存中間結果。
用「某種唯一 key」把同一種東西歸在一起。
最後用 Object.values() 把 Map 轉回陣列。[1]
在單元測試裡,先想清楚工作單元的進入點和退出點,再設計測試案例:不同退出點通常代表不同需求。
好的單元測試應該可讀、可重複、可自動,並且盡量在記憶體內同步運行,不依賴外部 I/O。
JavaScript 的 async/await 是把非同步邏輯以同步形式表達的語法糖,有助於讓測試流程更線性。
在「統計」這種問題上,Object 當 Map + Object.values 是非常實用的模式,時間複雜度從 O(n²) 降到 O(n)。[2][1]
檢查物件屬性存在時,要根據情境選擇:
自己控制的資料 + 值不會是 falsy:if (obj[key]) 就夠用。
嚴謹、泛用:Object.hasOwn(obj, key) 或 Object.prototype.hasOwnProperty.call(obj, key),並搭配 ESLint 規則避免陷阱。[9][7]
如果之後想把這段統計邏輯加上單元測試,下一步就可以練習:為 countEquipments 設計進入點(呼叫函式)、退出點(回傳值)與幾組不同情境的 test cases。
Share Dialog
Share Dialog
No comments yet