線上課程觀課進度管理小工具開發日誌
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 Visualized - Promise Execution

JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
影片版: JavaScript Visualized: Promise Execution
文字版: JavaScript Visualized: Promise Execution

Promise 執行順序重點
Constructor 是同步的;executor 會立刻進入 Call Stack 執行。
then/catch/finally 的回呼排入 Microtask Queue;setTimeout 等 Web APIs 的回呼排入 Task Queue(Macro task)。
Event Loop 順序:清空 Call Stack → 取出所有 Microtasks → 取出一個 Task → 可能觸發渲染再循環。
影片版: JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
文字版: JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue

callback based
promise based
Uncle Bob 為這本書的內容錄製了幾支示範影片,台灣讀者在博碩出版商網站可以下載影片檔、含時間戳的 txt 檔和第五章的範例程式碼 txt 檔(方便直接複製到 IDE 閱讀和執行體驗該章「重構」的內容)。
第一支影片用 Java 實作一個 stack 示範實作「在幾秒鐘內完成紅燈→綠燈→重構」循環。 照著書中建議先看影片再看文字說明,深刻感受到看影片體驗到的節奏感真的不是文字描述可以表達的。也發現我前面幾天自己看文件理解練習的 TDD 是「邏輯對了,實作流程不太對」的版本:
我把一題裡面會用到的 function 的測試全部寫完(包含定義、輸入、預期輸出、例外),才去寫產品程式碼中的 function
這樣整塊整塊做確實減少很多切換視窗的麻煩,但沒有完整跑過「編譯錯誤→測試行為→清理程式碼/重構」的思考邏輯。
我現在的理解是:TDD 這個方法核心概念是要把寫程式碼的思考和實作步驟拆到幾行以內,確保跨出每一個小步都是正確合理的,只要通過測試的程式碼就是可以安全部署 (deploy) 的程式碼。也企圖以此儘可能避免「一個看起來可以動的程式碼」後續要大量 debug 的工作。
今天的 TDD 練習:
以 JavaScript function 完成整數 queue 的 circular buffer,物件導向的寫法之後再說😆
寫起來還是滿卡的,思路不是很順,要習慣一下每次只推進幾行的思考方式
書中題目敘述:
按照示範的方式實作一個先進先出的整數堆疊 (queue) 。請使用固定大小的陣列來存放整數。你可能會需要兩個指標 (pointer) 來追蹤元素的新增位置和移除位置。完成後,你會發現自己實作了一個循環緩衝區 (circular buffer)。
資料結構(Plain Object):
{
buffer: [], // 固定大小陣列
capacity: 3, // 容量上限
headIndex: 0, // 讀取位置
tailIndex: 0, // 寫入位置
size: 0 // 當前元素數量
}
繞圈(Wrap Around)核心:以模運算限制索引在 0..capacity-1
tailIndex = (tailIndex + 1) % capacity
headIndex = (headIndex + 1) % capacity
JavaScript 的餘數運算子 % 可用於此,確保索引不會超出陣列邊界並能回到 0。
Step 1: 建立空 Queue
測試目標:size === 0、isEmpty(queue) === true、isFull(queue) === false。
語言特性參考:陣列與物件屬性皆為標準內建類型與結構。
Step 2: 單一 Enqueue / Dequeue (Happy Path)
測試目標:enqueue 後 size === 1 且 peek(queue) 為該值;dequeue 能拿回該值並回到空。
Debug 經驗:
ReferenceError: peak is not defined → 函式命名誤用(應為 peek)。
取得 undefined → 少了真正的賦值動作(queue.buffer[queue.tailIndex] = value),屬於語法疏漏由測試攔截。
Step 3: 邊界條件 (Edge Cases)
isFull(queue) 當 size === capacity 時為真;isEmpty(queue) 當 size === 0 時為真。
空取值/滿插入應拋出錯誤(queue is empty / queue is full),測試用 toThrow 斷言此行為。
Step 4: 整合測試 - 繞圈行為 (Circular Buffer Logic)
測試描述:在 buffer 內多次反覆填值和取值,部分取出與再填入的次數不規則時,仍能按照先進先出的正確順序取值。
驗證重點:tailIndex / headIndex 超過末端後能回到 index 0 繼續運作,取出順序保持 FIFO。
每個 it 皆建立自己的 queue 實例,避免測試間狀態汙染;必要時使用 beforeEach 於每次測試前建立新實例。
函式版 API 時需呼叫函式判斷狀態:isEmpty(queue) / isFull(queue);非 queue.isEmpty / queue.isFull(除非以 class getter 設計)。
常見斷言:
等值:expect(value).toBe(expected)。
丟錯:expect(() => fn()).toThrow('message')。
參考文件:
Vitest API 參考(describe/it/expect/toThrow 等):https://vitest.dev/api/
測試常見錯誤與隔離原則:Vitest Common Errors / Environment。
MDN JavaScript 指南與資料結構(物件、陣列、型別):MDN JS / 資料型別與資料結構 / Array 參考
餘數(%)運算子(繞圈核心運算):Remainder (%) - MDN
// int-queue.test.js
import { describe, expect, it } from 'vitest';
import {
createIntQueue,
isEmpty,
isFull,
enqueue,
dequeue,
peek,
} from './int-queue.js';
describe(`IntQueue`, ()=>{
it(`create an empty queue`, ()=>{
const queue = createIntQueue(3);
expect(queue.size).toBe(0);
expect(isEmpty(queue)).toBe(true);
expect(isFull(queue)).toBe(false)
})
})
it(`enqueues and dequeues one value`,()=>{
const queue = createIntQueue(3);
enqueue(queue, 42);
expect(queue.size).toBe(1)
expect(isEmpty(queue)).toBe(false)
expect(peek(queue)).toBe(42)
const value = dequeue(queue);
expect(value).toBe(42)
expect(queue.size).toBe(0)
expect(isEmpty(queue)).toBe(true)
})
it('becomes full after enqueuing capacity times',()=>{
const queue = createIntQueue(3);
expect(isFull(queue)).toBe(false);
enqueue(queue, 1);
enqueue(queue, 2);
enqueue(queue, 3);
expect(isFull(queue)).toBe(true);
expect(queue.size).toBe(3);
})
it('throws error when enqueue to a full queue',()=>{
const queue = createIntQueue(3);
enqueue(queue, 1);
enqueue(queue, 2);
enqueue(queue, 3);
expect(() => enqueue(queue, 4)).toThrowError('queue is full');
});
it('throws error when dequeue from an empty queue',()=>{
const queue = createIntQueue(3);
expect(() => dequeue(queue)).toThrowError('queue is empty');
});
it('在多次不規則的新增與取出後,索引繞圈時仍維持先進先出的正確順序', () => {
const queue = createIntQueue(3)
// 初次填入兩個
enqueue(queue, 1)
enqueue(queue, 2)
expect(dequeue(queue)).toBe(1) // head: 0 -> 1,取出 1
expect(peek(queue)).toBe(2) // 目前隊首是 2
// 填滿剩下的空間,讓 tail 繞回 index 0
enqueue(queue, 3) // buffer: [ , 2, 3],head = 1, tail = 0
enqueue(queue, 4) // buffer: [4, 2, 3],head = 1, tail = 1 (繞回到 0 再往前)
// 依序取出,確認順序仍然正確
expect(dequeue(queue)).toBe(2) // head: 1 -> 2,取出 2
expect(dequeue(queue)).toBe(3) // head: 2 -> 0,取出 3(head 繞回 0)
expect(dequeue(queue)).toBe(4) // head: 0 -> 1,取出 4
expect(isEmpty(queue)).toBe(true)
})
// int-queue.js
export function createIntQueue(capacity){
if(!Number.isInteger(capacity)||capacity<=0){
throw new Error('capacity must be a positive integer')
}
return {buffer:new Array(capacity),
capacity,
headIndex:0,
tailIndex:0,
size:0
}
}
export function isEmpty(queue){
return queue.size === 0;
}
export function isFull(queue){
return queue.size === queue.capacity;
}
export function enqueue(queue, value){
if(!Number.isInteger(value)){
throw new Error('value must be an integer');
}
if (isFull(queue)){
throw new Error('queue is full');
}
queue.buffer[queue.tailIndex]=value;
queue.tailIndex = (queue.tailIndex + 1) % queue.capacity;
queue.size += 1;
}
export function peek(queue){
if (isEmpty(queue)){
throw new Error('queue is empty');
}
return queue.buffer[queue.headIndex];
}
export function dequeue(queue){
if(isEmpty(queue)){
throw new Error('queue is empty');
}
const value = queue.buffer[queue.headIndex];
queue.buffer[queue.headIndex]=undefined;
queue.headIndex=(queue.headIndex + 1) % queue.capacity;
queue.size -= 1;
return value;
}

JavaScript Visualized - Promise Execution

JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
影片版: JavaScript Visualized: Promise Execution
文字版: JavaScript Visualized: Promise Execution

Promise 執行順序重點
Constructor 是同步的;executor 會立刻進入 Call Stack 執行。
then/catch/finally 的回呼排入 Microtask Queue;setTimeout 等 Web APIs 的回呼排入 Task Queue(Macro task)。
Event Loop 順序:清空 Call Stack → 取出所有 Microtasks → 取出一個 Task → 可能觸發渲染再循環。
影片版: JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
文字版: JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue

callback based
promise based
Uncle Bob 為這本書的內容錄製了幾支示範影片,台灣讀者在博碩出版商網站可以下載影片檔、含時間戳的 txt 檔和第五章的範例程式碼 txt 檔(方便直接複製到 IDE 閱讀和執行體驗該章「重構」的內容)。
第一支影片用 Java 實作一個 stack 示範實作「在幾秒鐘內完成紅燈→綠燈→重構」循環。 照著書中建議先看影片再看文字說明,深刻感受到看影片體驗到的節奏感真的不是文字描述可以表達的。也發現我前面幾天自己看文件理解練習的 TDD 是「邏輯對了,實作流程不太對」的版本:
我把一題裡面會用到的 function 的測試全部寫完(包含定義、輸入、預期輸出、例外),才去寫產品程式碼中的 function
這樣整塊整塊做確實減少很多切換視窗的麻煩,但沒有完整跑過「編譯錯誤→測試行為→清理程式碼/重構」的思考邏輯。
我現在的理解是:TDD 這個方法核心概念是要把寫程式碼的思考和實作步驟拆到幾行以內,確保跨出每一個小步都是正確合理的,只要通過測試的程式碼就是可以安全部署 (deploy) 的程式碼。也企圖以此儘可能避免「一個看起來可以動的程式碼」後續要大量 debug 的工作。
今天的 TDD 練習:
以 JavaScript function 完成整數 queue 的 circular buffer,物件導向的寫法之後再說😆
寫起來還是滿卡的,思路不是很順,要習慣一下每次只推進幾行的思考方式
書中題目敘述:
按照示範的方式實作一個先進先出的整數堆疊 (queue) 。請使用固定大小的陣列來存放整數。你可能會需要兩個指標 (pointer) 來追蹤元素的新增位置和移除位置。完成後,你會發現自己實作了一個循環緩衝區 (circular buffer)。
資料結構(Plain Object):
{
buffer: [], // 固定大小陣列
capacity: 3, // 容量上限
headIndex: 0, // 讀取位置
tailIndex: 0, // 寫入位置
size: 0 // 當前元素數量
}
繞圈(Wrap Around)核心:以模運算限制索引在 0..capacity-1
tailIndex = (tailIndex + 1) % capacity
headIndex = (headIndex + 1) % capacity
JavaScript 的餘數運算子 % 可用於此,確保索引不會超出陣列邊界並能回到 0。
Step 1: 建立空 Queue
測試目標:size === 0、isEmpty(queue) === true、isFull(queue) === false。
語言特性參考:陣列與物件屬性皆為標準內建類型與結構。
Step 2: 單一 Enqueue / Dequeue (Happy Path)
測試目標:enqueue 後 size === 1 且 peek(queue) 為該值;dequeue 能拿回該值並回到空。
Debug 經驗:
ReferenceError: peak is not defined → 函式命名誤用(應為 peek)。
取得 undefined → 少了真正的賦值動作(queue.buffer[queue.tailIndex] = value),屬於語法疏漏由測試攔截。
Step 3: 邊界條件 (Edge Cases)
isFull(queue) 當 size === capacity 時為真;isEmpty(queue) 當 size === 0 時為真。
空取值/滿插入應拋出錯誤(queue is empty / queue is full),測試用 toThrow 斷言此行為。
Step 4: 整合測試 - 繞圈行為 (Circular Buffer Logic)
測試描述:在 buffer 內多次反覆填值和取值,部分取出與再填入的次數不規則時,仍能按照先進先出的正確順序取值。
驗證重點:tailIndex / headIndex 超過末端後能回到 index 0 繼續運作,取出順序保持 FIFO。
每個 it 皆建立自己的 queue 實例,避免測試間狀態汙染;必要時使用 beforeEach 於每次測試前建立新實例。
函式版 API 時需呼叫函式判斷狀態:isEmpty(queue) / isFull(queue);非 queue.isEmpty / queue.isFull(除非以 class getter 設計)。
常見斷言:
等值:expect(value).toBe(expected)。
丟錯:expect(() => fn()).toThrow('message')。
參考文件:
Vitest API 參考(describe/it/expect/toThrow 等):https://vitest.dev/api/
測試常見錯誤與隔離原則:Vitest Common Errors / Environment。
MDN JavaScript 指南與資料結構(物件、陣列、型別):MDN JS / 資料型別與資料結構 / Array 參考
餘數(%)運算子(繞圈核心運算):Remainder (%) - MDN
// int-queue.test.js
import { describe, expect, it } from 'vitest';
import {
createIntQueue,
isEmpty,
isFull,
enqueue,
dequeue,
peek,
} from './int-queue.js';
describe(`IntQueue`, ()=>{
it(`create an empty queue`, ()=>{
const queue = createIntQueue(3);
expect(queue.size).toBe(0);
expect(isEmpty(queue)).toBe(true);
expect(isFull(queue)).toBe(false)
})
})
it(`enqueues and dequeues one value`,()=>{
const queue = createIntQueue(3);
enqueue(queue, 42);
expect(queue.size).toBe(1)
expect(isEmpty(queue)).toBe(false)
expect(peek(queue)).toBe(42)
const value = dequeue(queue);
expect(value).toBe(42)
expect(queue.size).toBe(0)
expect(isEmpty(queue)).toBe(true)
})
it('becomes full after enqueuing capacity times',()=>{
const queue = createIntQueue(3);
expect(isFull(queue)).toBe(false);
enqueue(queue, 1);
enqueue(queue, 2);
enqueue(queue, 3);
expect(isFull(queue)).toBe(true);
expect(queue.size).toBe(3);
})
it('throws error when enqueue to a full queue',()=>{
const queue = createIntQueue(3);
enqueue(queue, 1);
enqueue(queue, 2);
enqueue(queue, 3);
expect(() => enqueue(queue, 4)).toThrowError('queue is full');
});
it('throws error when dequeue from an empty queue',()=>{
const queue = createIntQueue(3);
expect(() => dequeue(queue)).toThrowError('queue is empty');
});
it('在多次不規則的新增與取出後,索引繞圈時仍維持先進先出的正確順序', () => {
const queue = createIntQueue(3)
// 初次填入兩個
enqueue(queue, 1)
enqueue(queue, 2)
expect(dequeue(queue)).toBe(1) // head: 0 -> 1,取出 1
expect(peek(queue)).toBe(2) // 目前隊首是 2
// 填滿剩下的空間,讓 tail 繞回 index 0
enqueue(queue, 3) // buffer: [ , 2, 3],head = 1, tail = 0
enqueue(queue, 4) // buffer: [4, 2, 3],head = 1, tail = 1 (繞回到 0 再往前)
// 依序取出,確認順序仍然正確
expect(dequeue(queue)).toBe(2) // head: 1 -> 2,取出 2
expect(dequeue(queue)).toBe(3) // head: 2 -> 0,取出 3(head 繞回 0)
expect(dequeue(queue)).toBe(4) // head: 0 -> 1,取出 4
expect(isEmpty(queue)).toBe(true)
})
// int-queue.js
export function createIntQueue(capacity){
if(!Number.isInteger(capacity)||capacity<=0){
throw new Error('capacity must be a positive integer')
}
return {buffer:new Array(capacity),
capacity,
headIndex:0,
tailIndex:0,
size:0
}
}
export function isEmpty(queue){
return queue.size === 0;
}
export function isFull(queue){
return queue.size === queue.capacity;
}
export function enqueue(queue, value){
if(!Number.isInteger(value)){
throw new Error('value must be an integer');
}
if (isFull(queue)){
throw new Error('queue is full');
}
queue.buffer[queue.tailIndex]=value;
queue.tailIndex = (queue.tailIndex + 1) % queue.capacity;
queue.size += 1;
}
export function peek(queue){
if (isEmpty(queue)){
throw new Error('queue is empty');
}
return queue.buffer[queue.headIndex];
}
export function dequeue(queue){
if(isEmpty(queue)){
throw new Error('queue is empty');
}
const value = queue.buffer[queue.headIndex];
queue.buffer[queue.headIndex]=undefined;
queue.headIndex=(queue.headIndex + 1) % queue.capacity;
queue.size -= 1;
return value;
}
Share Dialog
Share Dialog
No comments yet