線上課程觀課進度管理小工具開發日誌
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」。
這篇筆記整理的是:
如何用 JavaScript 原生資料結構(Array / Object / Map / Set)搭配 Array.prototype.sort、Fisher-Yates 洗牌,實作一個小型 CLI「顏色抽獎」模擬程式 。 developer.mozilla
過程中會刻意用 TDD 思維,把邏輯拆成小函式,一步步構出整體流程。
想像一個開源的 JavaScript Kata:「Color Raffle」:
有最多 50 個參加者,每人身上有 3 個彩色標籤。
標籤有 5 種顏色,每種顏色一開始各準備 30 個(總共 150 個標籤)。
遊戲過程中玩家互相交換標籤,最後每個人的身上都有 3 個顏色(可以重複)。
主辦單位進行抽獎:
統計每種「顏色組合」出現幾個人(例如「紅-藍-綠」有 5 人)。
顯示所有組合及人數,並依人數排序。
再統計一次五種顏色目前各自出現幾次,確認總數仍是 150 個。
這個 Kata 的重點不是「模擬交換過程」,而是練習幾個關鍵技能 : developer.mozilla
用 Array / Object / Map / Set 表達不同型態的資料。
用 Map 做聚合統計(aggregation)。
用 Array.prototype.sort 做多條件排序 。 cs.unb
實作 Fisher-Yates 洗牌演算法,生成隨機分布 。 en.wikipedia
用 TDD 把大問題拆成小函式。
為了讓程式容易測試與維護,可以先用自然語言拆出五個函式:
generateDancers(dancerCount)
產生隨機參加者及其顏色組合(不寫測試,因為帶隨機性)。
countColorTotals(dancers)
統計五種顏色各自出現幾次(TDD)。
countCombinations(dancers)
統計每種顏色組合出現幾個人(TDD)。
sortCombinations(combinationsMap)
將組合 Map 轉成排序後的陣列,方便輸出(TDD)。 developer.mozilla
main()
串起整個流程,作為 CLI 入口(不寫測試)。
檔案結構可以這樣規劃:
color-raffle-kata/
├── core.js // 核心邏輯
├── core.test.js // TDD 測試
├── main.js // CLI 入口
└── package.json
輸入:dancerCount: number,例如 50。
輸出:Array<Array<string>>,每個元素代表一位參加者的三個顏色,例如:
[ ['red', 'blue', 'green'],
['red', 'red', 'yellow'],
// ...
]
顏色種類:['red', 'blue', 'green', 'yellow', 'purple']。
每種顏色放入 30 個標籤。
總標籤數:dancerCount * 3(預設 50 人 → 150 個)。
流程:
建立顏色池 pool(150 個元素)。
使用 Fisher-Yates 演算法洗牌 pool 。 en.wikipedia
每 3 個標籤分配給一個參加者。
Fisher-Yates 是一個經典的、無偏差(unbiased)的洗牌演算法,適合用來隨機排列有限長度陣列 。 stackoverflow
實作(原地修改陣列):
/**
* Fisher-Yates 洗牌演算法(原地修改 array)
* @param {Array} array - 要打亂的陣列(會直接修改原陣列)
* @returns {void}
*/
export function shuffle(array) {
for (let i = array.length - 1; i > 0; i -= 1) {
const randomIndex = Math.floor(Math.random() * (i + 1));
[array[i], array[randomIndex]] = [array[randomIndex], array[i]];
}
}
幾個重點:
迴圈從最後一個索引往前走。
每次在 [0, i] 範圍內選出一個 randomIndex,與 i 位置交換 。 en.wikipedia
Math.random() * (i + 1) 產生 [0, i+1) 範圍的浮點數,再用 Math.floor 轉成 [0, i] 的整數 。 tc39
這是 in-place 演算法:不會回傳新陣列,而是直接修改傳入的陣列 。 en.wikipedia
export function generateDancers(dancerCount) {
const pool = [];
const colors = ['red', 'blue', 'green', 'yellow', 'purple'];
const tokensPerColor = 30;
// 建立顏色池
colors.forEach((color) => {
for (let i = 0; i < tokensPerColor; i += 1) {
pool.push(color);
}
});
// 洗牌(原地修改 pool)
shuffle(pool);
// 每 3 個顏色分配給一個參加者
const dancers = [];
for (let i = 0; i < dancerCount; i += 1) {
dancers.push(pool.slice(i * 3, i * 3 + 3));
}
return dancers;
}
這個函式因為帶有隨機性,可以用手動印出、簡單檢查總數與分布,而不一定用單元測試檢查具體值。
輸入:dancers: Array<Array<string>>。
輸出:一個物件,紀錄每個顏色出現次數:
{
red: number,
blue: number,
green: number,
yellow: number,
purple: number,
}
空陣列:
輸入:[]
輸出:所有顏色 0。
單一參加者、三個不同顏色:
輸入:[['red', 'blue', 'green']]
輸出:red/blue/green 各 1,yellow/purple 為 0。
多個參加者顏色重複:
輸入:
[ ['red', 'blue', 'green'],
['red', 'red', 'red'],
['blue', 'green', 'yellow'],
]
輸出:
{ red: 4, blue: 2, green: 2, yellow: 1, purple: 0 }
export function countColorTotals(dancers) {
const counts = {
red: 0,
blue: 0,
green: 0,
yellow: 0,
purple: 0,
};
dancers.forEach((dancer) => {
dancer.forEach((color) => {
counts[color] += 1;
});
});
return counts;
}
這裡 Object 是自然的選擇,因為:
key 集合是固定、已知的一組字串 。 developer.mozilla
查找 counts[color] 平均是 O(1) 。 tc39
統計每種顏色組合有幾個人,順序不同但組合相同要算在一起。
輸入:dancers: Array<Array<string>>
const dancers = [ ['red', 'blue', 'green'],
['green', 'red', 'blue'],
['red', 'red', 'red'],
];
輸出:Map<string, number>
Map {
'blue-green-red' => 2,
'red-red-red' => 1,
}
這裡的 key 是「顏色陣列」轉成的字串,例如 'blue-green-red'。
可以用 Object 當作 map,但 Map API 的語意更清楚:
map.has(key)、map.get(key)、map.set(key, value) 。 developer.mozilla
map.size 直接給出 key 的個數 。 developer.mozilla
同一種組合即使順序不同:
['red', 'blue', 'green']
['green', 'red', 'blue']
都應該被當成同一 key。
標準化策略:
複製顏色陣列(避免修改原資料)。
排序(使用 Array.prototype.sort(),依字串的 Unicode/UTF-16 編碼排序)。 developer.mozilla
用 '-' join 成字串當作 key。
空陣列 → 回傳空 Map。
單一參加者 → Map.size === 1,該 key 的 count 為 1。
兩個參加者顏色順序不同但組合相同 → Map.size === 1,count 為 2。
多個參加者、組合都不同 → Map.size 等於組合種類數,各自 count 正確。
export function countCombinations(dancers) {
const combinations = new Map();
dancers.forEach((dancer) => {
const key = dancer.slice().sort().join('-');
if (combinations.has(key)) {
combinations.set(key, combinations.get(key) + 1);
} else {
combinations.set(key, 1);
}
});
return combinations;
}
注意:
dancer.slice().sort():先用 slice() 建立副本,再 sort,避免直接修改原陣列 。 developer.mozilla
用 Map.prototype.has、get、set 來維護計數 。 developer.mozilla
輸入:Map<string, number>。
輸出:排好序的陣列,每個元素形如 { combo: string, count: number }。
範例輸入:
const combinationsMap = new Map([ ['blue-green-red', 5],
['red-red-red', 8],
['green-yellow-purple', 3],
]);
範例輸出(依 count 降序):
[
{ combo: 'red-red-red', count: 8 },
{ combo: 'blue-green-red', count: 5 },
{ combo: 'green-yellow-purple', count: 3 },
]
目前需求只規定「依人數排序」,沒有要求人數相同時的順序,因此不額外實作次要排序,避免過度設計 。 developer.mozilla
const result = [];
combinationsMap.forEach((value, key) => {
result.push({ combo: key, count: value });
});
也可以用 entries() + 展開運算子:
const result = [...combinationsMap.entries()].map(([key, value]) => ({
combo: key,
count: value,
}));
Map.prototype.entries() 回傳的是 iterator,展開運算子 ... 會依插入順序消耗 iterator 的值,轉成陣列 。 developer.mozilla
Array.prototype.sort(compareFn) 的比較函式規則 : cs.unb
回傳值 < 0 → a 排在 b 前面。
回傳值 > 0 → a 排在 b 後面。
回傳值 === 0 → 相對順序不變。
要做「count 降序」:
當 a.count > b.count 時,希望 a 在前 → 讓 compareFn(a, b) 回傳負數。
寫成算式就是 b.count - a.count。
實作:
export function sortCombinations(combinationsMap) {
const result = [];
combinationsMap.forEach((value, key) => {
result.push({ combo: key, count: value });
});
result.sort((combination1, combination2) =>
combination2.count - combination1.count
);
return result;
}
TDD 測試可以檢查:
空 Map → 回傳空陣列。
單一 entry → 陣列長度 1,內容正確。
多個 entry、count 都不同 → 按 count 降序排列。
最後用一個 main() 把整個流程串起來,當作 CLI 入口(例如用 Node.js 執行):
export function main() {
const dancerCount = 50;
const dancers = generateDancers(dancerCount);
const combinationsMap = countCombinations(dancers);
const sortedCombinations = sortCombinations(combinationsMap);
const colorTotals = countColorTotals(dancers);
console.log('Color combinations (sorted by count):');
sortedCombinations.forEach((entry) => {
console.log(`${entry.combo}: ${entry.count} people`);
});
console.log('\nColor totals:');
let total = 0;
Object.entries(colorTotals).forEach(([color, count]) => {
console.log(`${color}: ${count} tokens`);
total += count;
});
console.log(`Total: ${total} tokens`);
if (total !== dancerCount * 3) {
console.error('Data inconsistency detected!');
}
}
這個函式本身通常不寫單元測試,而是用手動執行檢查整體行為。
在這個 Kata 裡,有幾個地方會碰到「原地(in-place)操作」 : geeksforgeeks
shuffle(array):直接修改傳入的陣列。
Array.prototype.sort():也是 in-place,會修改原陣列 。 cs.unb
設計時可以注意:
命名要誠實
例如 shuffle 或 shuffleInPlace,清楚表達它會修改原陣列。
內部變數可以 in-place
像 generateDancers 裡的 pool 是函式內部變數,外部看不到原始值,對它 in-place 洗牌是安全的。
對輸入資料要小心
像 countCombinations 中不能直接 sort 呼叫方傳入的 dancer,要用 dancer.slice().sort() 先複製再操作 。 developer.mozilla
這個 Color Raffle Kata,其實是一個蠻好的 JS 資料結構與語言機制綜合練習:
用 Array 存 list,用 Object 當固定 key 的計數器,用 Map 當動態 key 的 map,用 Set 處理唯一值集合 。 developer.mozilla
用 Map.prototype.forEach、entries() + 展開運算子、Object.entries / Object.values 在資料結構之間切換 。 alexop
用 Array.prototype.sort 的比較函式做降序排序與多條件排序 。 developer.mozilla
用 Fisher-Yates 洗牌做公平隨機排列,而不是 sort(() => Math.random() - 0.5) 這類有偏差的寫法 。 ronluo.coderbridge
意識到哪些操作是 in-place,避免不小心修改呼叫方傳入的資料 。 en.wikipedia
用 TDD 把功能拆小:先設計輸入輸出,再實作最小邏輯,最後才組合成 CLI。
這樣整理完之後,下次要重用這個模式(例如票券組合統計、產品選配組合統計)時,只要換掉顏色和情境,整套邏輯幾乎可以原封帶走。
這篇筆記整理的是:
如何用 JavaScript 原生資料結構(Array / Object / Map / Set)搭配 Array.prototype.sort、Fisher-Yates 洗牌,實作一個小型 CLI「顏色抽獎」模擬程式 。 developer.mozilla
過程中會刻意用 TDD 思維,把邏輯拆成小函式,一步步構出整體流程。
想像一個開源的 JavaScript Kata:「Color Raffle」:
有最多 50 個參加者,每人身上有 3 個彩色標籤。
標籤有 5 種顏色,每種顏色一開始各準備 30 個(總共 150 個標籤)。
遊戲過程中玩家互相交換標籤,最後每個人的身上都有 3 個顏色(可以重複)。
主辦單位進行抽獎:
統計每種「顏色組合」出現幾個人(例如「紅-藍-綠」有 5 人)。
顯示所有組合及人數,並依人數排序。
再統計一次五種顏色目前各自出現幾次,確認總數仍是 150 個。
這個 Kata 的重點不是「模擬交換過程」,而是練習幾個關鍵技能 : developer.mozilla
用 Array / Object / Map / Set 表達不同型態的資料。
用 Map 做聚合統計(aggregation)。
用 Array.prototype.sort 做多條件排序 。 cs.unb
實作 Fisher-Yates 洗牌演算法,生成隨機分布 。 en.wikipedia
用 TDD 把大問題拆成小函式。
為了讓程式容易測試與維護,可以先用自然語言拆出五個函式:
generateDancers(dancerCount)
產生隨機參加者及其顏色組合(不寫測試,因為帶隨機性)。
countColorTotals(dancers)
統計五種顏色各自出現幾次(TDD)。
countCombinations(dancers)
統計每種顏色組合出現幾個人(TDD)。
sortCombinations(combinationsMap)
將組合 Map 轉成排序後的陣列,方便輸出(TDD)。 developer.mozilla
main()
串起整個流程,作為 CLI 入口(不寫測試)。
檔案結構可以這樣規劃:
color-raffle-kata/
├── core.js // 核心邏輯
├── core.test.js // TDD 測試
├── main.js // CLI 入口
└── package.json
輸入:dancerCount: number,例如 50。
輸出:Array<Array<string>>,每個元素代表一位參加者的三個顏色,例如:
[ ['red', 'blue', 'green'],
['red', 'red', 'yellow'],
// ...
]
顏色種類:['red', 'blue', 'green', 'yellow', 'purple']。
每種顏色放入 30 個標籤。
總標籤數:dancerCount * 3(預設 50 人 → 150 個)。
流程:
建立顏色池 pool(150 個元素)。
使用 Fisher-Yates 演算法洗牌 pool 。 en.wikipedia
每 3 個標籤分配給一個參加者。
Fisher-Yates 是一個經典的、無偏差(unbiased)的洗牌演算法,適合用來隨機排列有限長度陣列 。 stackoverflow
實作(原地修改陣列):
/**
* Fisher-Yates 洗牌演算法(原地修改 array)
* @param {Array} array - 要打亂的陣列(會直接修改原陣列)
* @returns {void}
*/
export function shuffle(array) {
for (let i = array.length - 1; i > 0; i -= 1) {
const randomIndex = Math.floor(Math.random() * (i + 1));
[array[i], array[randomIndex]] = [array[randomIndex], array[i]];
}
}
幾個重點:
迴圈從最後一個索引往前走。
每次在 [0, i] 範圍內選出一個 randomIndex,與 i 位置交換 。 en.wikipedia
Math.random() * (i + 1) 產生 [0, i+1) 範圍的浮點數,再用 Math.floor 轉成 [0, i] 的整數 。 tc39
這是 in-place 演算法:不會回傳新陣列,而是直接修改傳入的陣列 。 en.wikipedia
export function generateDancers(dancerCount) {
const pool = [];
const colors = ['red', 'blue', 'green', 'yellow', 'purple'];
const tokensPerColor = 30;
// 建立顏色池
colors.forEach((color) => {
for (let i = 0; i < tokensPerColor; i += 1) {
pool.push(color);
}
});
// 洗牌(原地修改 pool)
shuffle(pool);
// 每 3 個顏色分配給一個參加者
const dancers = [];
for (let i = 0; i < dancerCount; i += 1) {
dancers.push(pool.slice(i * 3, i * 3 + 3));
}
return dancers;
}
這個函式因為帶有隨機性,可以用手動印出、簡單檢查總數與分布,而不一定用單元測試檢查具體值。
輸入:dancers: Array<Array<string>>。
輸出:一個物件,紀錄每個顏色出現次數:
{
red: number,
blue: number,
green: number,
yellow: number,
purple: number,
}
空陣列:
輸入:[]
輸出:所有顏色 0。
單一參加者、三個不同顏色:
輸入:[['red', 'blue', 'green']]
輸出:red/blue/green 各 1,yellow/purple 為 0。
多個參加者顏色重複:
輸入:
[ ['red', 'blue', 'green'],
['red', 'red', 'red'],
['blue', 'green', 'yellow'],
]
輸出:
{ red: 4, blue: 2, green: 2, yellow: 1, purple: 0 }
export function countColorTotals(dancers) {
const counts = {
red: 0,
blue: 0,
green: 0,
yellow: 0,
purple: 0,
};
dancers.forEach((dancer) => {
dancer.forEach((color) => {
counts[color] += 1;
});
});
return counts;
}
這裡 Object 是自然的選擇,因為:
key 集合是固定、已知的一組字串 。 developer.mozilla
查找 counts[color] 平均是 O(1) 。 tc39
統計每種顏色組合有幾個人,順序不同但組合相同要算在一起。
輸入:dancers: Array<Array<string>>
const dancers = [ ['red', 'blue', 'green'],
['green', 'red', 'blue'],
['red', 'red', 'red'],
];
輸出:Map<string, number>
Map {
'blue-green-red' => 2,
'red-red-red' => 1,
}
這裡的 key 是「顏色陣列」轉成的字串,例如 'blue-green-red'。
可以用 Object 當作 map,但 Map API 的語意更清楚:
map.has(key)、map.get(key)、map.set(key, value) 。 developer.mozilla
map.size 直接給出 key 的個數 。 developer.mozilla
同一種組合即使順序不同:
['red', 'blue', 'green']
['green', 'red', 'blue']
都應該被當成同一 key。
標準化策略:
複製顏色陣列(避免修改原資料)。
排序(使用 Array.prototype.sort(),依字串的 Unicode/UTF-16 編碼排序)。 developer.mozilla
用 '-' join 成字串當作 key。
空陣列 → 回傳空 Map。
單一參加者 → Map.size === 1,該 key 的 count 為 1。
兩個參加者顏色順序不同但組合相同 → Map.size === 1,count 為 2。
多個參加者、組合都不同 → Map.size 等於組合種類數,各自 count 正確。
export function countCombinations(dancers) {
const combinations = new Map();
dancers.forEach((dancer) => {
const key = dancer.slice().sort().join('-');
if (combinations.has(key)) {
combinations.set(key, combinations.get(key) + 1);
} else {
combinations.set(key, 1);
}
});
return combinations;
}
注意:
dancer.slice().sort():先用 slice() 建立副本,再 sort,避免直接修改原陣列 。 developer.mozilla
用 Map.prototype.has、get、set 來維護計數 。 developer.mozilla
輸入:Map<string, number>。
輸出:排好序的陣列,每個元素形如 { combo: string, count: number }。
範例輸入:
const combinationsMap = new Map([ ['blue-green-red', 5],
['red-red-red', 8],
['green-yellow-purple', 3],
]);
範例輸出(依 count 降序):
[
{ combo: 'red-red-red', count: 8 },
{ combo: 'blue-green-red', count: 5 },
{ combo: 'green-yellow-purple', count: 3 },
]
目前需求只規定「依人數排序」,沒有要求人數相同時的順序,因此不額外實作次要排序,避免過度設計 。 developer.mozilla
const result = [];
combinationsMap.forEach((value, key) => {
result.push({ combo: key, count: value });
});
也可以用 entries() + 展開運算子:
const result = [...combinationsMap.entries()].map(([key, value]) => ({
combo: key,
count: value,
}));
Map.prototype.entries() 回傳的是 iterator,展開運算子 ... 會依插入順序消耗 iterator 的值,轉成陣列 。 developer.mozilla
Array.prototype.sort(compareFn) 的比較函式規則 : cs.unb
回傳值 < 0 → a 排在 b 前面。
回傳值 > 0 → a 排在 b 後面。
回傳值 === 0 → 相對順序不變。
要做「count 降序」:
當 a.count > b.count 時,希望 a 在前 → 讓 compareFn(a, b) 回傳負數。
寫成算式就是 b.count - a.count。
實作:
export function sortCombinations(combinationsMap) {
const result = [];
combinationsMap.forEach((value, key) => {
result.push({ combo: key, count: value });
});
result.sort((combination1, combination2) =>
combination2.count - combination1.count
);
return result;
}
TDD 測試可以檢查:
空 Map → 回傳空陣列。
單一 entry → 陣列長度 1,內容正確。
多個 entry、count 都不同 → 按 count 降序排列。
最後用一個 main() 把整個流程串起來,當作 CLI 入口(例如用 Node.js 執行):
export function main() {
const dancerCount = 50;
const dancers = generateDancers(dancerCount);
const combinationsMap = countCombinations(dancers);
const sortedCombinations = sortCombinations(combinationsMap);
const colorTotals = countColorTotals(dancers);
console.log('Color combinations (sorted by count):');
sortedCombinations.forEach((entry) => {
console.log(`${entry.combo}: ${entry.count} people`);
});
console.log('\nColor totals:');
let total = 0;
Object.entries(colorTotals).forEach(([color, count]) => {
console.log(`${color}: ${count} tokens`);
total += count;
});
console.log(`Total: ${total} tokens`);
if (total !== dancerCount * 3) {
console.error('Data inconsistency detected!');
}
}
這個函式本身通常不寫單元測試,而是用手動執行檢查整體行為。
在這個 Kata 裡,有幾個地方會碰到「原地(in-place)操作」 : geeksforgeeks
shuffle(array):直接修改傳入的陣列。
Array.prototype.sort():也是 in-place,會修改原陣列 。 cs.unb
設計時可以注意:
命名要誠實
例如 shuffle 或 shuffleInPlace,清楚表達它會修改原陣列。
內部變數可以 in-place
像 generateDancers 裡的 pool 是函式內部變數,外部看不到原始值,對它 in-place 洗牌是安全的。
對輸入資料要小心
像 countCombinations 中不能直接 sort 呼叫方傳入的 dancer,要用 dancer.slice().sort() 先複製再操作 。 developer.mozilla
這個 Color Raffle Kata,其實是一個蠻好的 JS 資料結構與語言機制綜合練習:
用 Array 存 list,用 Object 當固定 key 的計數器,用 Map 當動態 key 的 map,用 Set 處理唯一值集合 。 developer.mozilla
用 Map.prototype.forEach、entries() + 展開運算子、Object.entries / Object.values 在資料結構之間切換 。 alexop
用 Array.prototype.sort 的比較函式做降序排序與多條件排序 。 developer.mozilla
用 Fisher-Yates 洗牌做公平隨機排列,而不是 sort(() => Math.random() - 0.5) 這類有偏差的寫法 。 ronluo.coderbridge
意識到哪些操作是 in-place,避免不小心修改呼叫方傳入的資料 。 en.wikipedia
用 TDD 把功能拆小:先設計輸入輸出,再實作最小邏輯,最後才組合成 CLI。
這樣整理完之後,下次要重用這個模式(例如票券組合統計、產品選配組合統計)時,只要換掉顏色和情境,整套邏輯幾乎可以原封帶走。
Share Dialog
Share Dialog
No comments yet