# Day 42-44:遞迴重構;TDD 流程;Caesar Cipher;Array.fill vs Array.from;分組計數;Map vs Object;解構賦值;JSDoc 型別;JSFuck **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-01-22 **URL:** https://paragraph.com/@gcake/day42-44 ## Content 整理日期: 2026/01/22一、遞迴函式程式碼重構:追求簡潔與可讀性原始問題:過度複雜的物件組裝當遞迴函式需要回傳多個值時,新手常會將所有資料包裝成物件,並額外建立一個包裝函式 。這會讓程式碼變得複雜且難以維護: jsdevspace.substack// ❌ 避免:巢狀函式與過度包裝 export function calculateFormula(n) { // 應避免在 function 內部定義 function(巢狀函式) const result = recursiveUnit(n); // 考慮在主程式層級組裝字串,而非在遞迴內部 return `${result.formula} = ${result.sum}`; } function recursiveUnit(cur) { if (cur === 4) { const value = 2 * 4; return { formula: `2*4`, sum: value, }; } const prev = recursiveUnit(cur - 2); const a = cur - 2; const b = cur; const value = a * b; return { formula: `${prev.formula} + ${a}*${b}`, sum: prev.sum + value, }; } 重構建議關鍵原則: 讓遞迴函式專注於單一職責,字串組裝交給外層處理 。 javascript.plainenglishFunction 建構式替代 eval如果需要動態執行字串運算,可使用 new Function() 建構式,它比 eval() 更安全,因為無法存取呼叫端的作用域 : stackoverflow// ✅ 較安全的動態執行方式 const calculate = new Function('a', 'b', 'return a * b'); console.log(calculate(3, 4)); // 12 Function 建構式只能存取全域作用域與傳入的參數,避免了 eval 可能存取外層變數的安全風險 。 dfkaye.github單一職責原則遞迴函式應該只負責計算,不要混雜格式化邏輯 : javascript.plainenglish// ✅ 推薦:職責分離 function calculateSum(n) { if (n === 4) return 2 * 4; return (n - 2) * n + calculateSum(n - 2); } // 格式化由外層處理 function formatResult(n) { const sum = calculateSum(n); return `計算結果 = ${sum}`; } 二、TDD 思考流程:先定義 I/O,再開始實作核心概念:黑盒子設計思維所有程式碼都應該用相同的思考流程:看到需求時,先定義清楚輸入(Input)與輸出(Output),實作不通再回頭調整計畫 。 moldstud TDD 強制先寫測試的本質,就是訓練「實作前先想清楚」的思維模式 。 moldstud關於程式碼重用的思考何時該抽出共用函式? 回到自然語言判斷「這是一件獨立的事」,而非單純看到重複程式碼就抽出來 。 moldstud✅ 正確思維: 「我要取奇數索引值的元素」是一件獨立且完整的事,後續被重複使用只是結果❌ 錯誤思維: 看到兩處程式碼一樣就立刻抽成函式程式碼的表達方式要考慮閱讀者感受,所以對「單獨一件事」的定義很重要。三、Caesar Cipher 加密演算法實作問題描述實作 Caesar Cipher 加密函式,將輸入字串的英文字母(a-z, A-Z)依指定位移量平移,其他字元保持原樣 。 caesar-cipher 需求:支援小寫、大寫字母處理字母包回(wrap around),如 xyz → abc非字母字元(數字、標點、空白)原樣回傳解題思路產生小寫字母陣列 lowerAlphabet(a-z)與大寫字母陣列 upperAlphabet(A-Z)對每個字元判斷:若是小寫 → 位移加密並處理包回若是大寫 → 位移加密並處理包回其他字元 → 原樣回傳使用 Array.prototype.map 處理字元陣列,最後用 join('') 組回字串 caesar-cipherJavaScript API 選用理由Array.from:產生字母陣列,搭配 String.fromCharCode 建立 Unicode 字元 delicious-insightssplit(''):將字串拆成字元陣列map:逐字元處理加密邏輯 developer.mozillaindexOf:判斷字元是否為英文字母並取得索引join(''):組回加密後字串實作程式碼export function caesarCipher(publicString, shift = 3) { // 產生 A-Z const upperAlphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i), ); // 產生 a-z const lowerAlphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode(97 + i), ); const privateString = publicString .split('') .map((char) => { if (lowerAlphabet.indexOf(char) !== -1) { const originIndex = lowerAlphabet.indexOf(char); const shiftIndex = (originIndex + shift) % 26; return lowerAlphabet[shiftIndex]; } else if (upperAlphabet.indexOf(char) !== -1) { const originIndex = upperAlphabet.indexOf(char); const shiftIndex = (originIndex + shift) % 26; return upperAlphabet[shiftIndex]; } return char; }) .join(''); return privateString; } 學習重點陣列操作: 熟悉 map、indexOf 與字串處理方法TDD 設計: 覆蓋正常、邊界、例外情境,確保程式碼健壯性 moldstud早期回傳(Guard Clause): 讓程式更易讀、易維護分類處理: 支援多種輸入時,先分類再處理,避免複雜的條件判斷四、Array.fill() vs Array.from() 的選擇時機核心概念:原始值 vs 物件參考原始值/純值(Primitive) developer.mozillastring、number、boolean、null、undefined、symbol、bigint賦值時複製值本身每個變數獨立,互不影響物件(Object)array、object、function賦值時複製參考(記憶體位址)多個變數可能指向同一物件,修改會互相影響 developer.mozillafill() 的陷阱fill(value) 只對 value 求值一次,然後把同一個值放進每個位置 。 developer.mozilla// ✅ 原始值:安全 const nums = Array(3).fill(0); nums[0] = 99; console.log(nums); // [99, 0, 0] ← 互不影響 // ❌ 物件:共享參考 const arrs = Array(3).fill([]); arrs[0].push("A"); console.log(arrs); // [["A"], ["A"], ["A"]] ← 全部都被改了! Array.from() 的安全性callback 在每次迭代時執行,每次產生新的獨立物件 。 delicious-insights// ✅ 每個元素是獨立的新陣列 const arrs = Array.from({ length: 3 }, () => []); arrs[0].push("A"); console.log(arrs); // [["A"], [], []] ← 只有第一個被改 判斷原則要填入的值是什麼? ├─ 原始值 → Array(n).fill(value) │ - 簡潔、效能較好 │ - 語意:「建立 n 個相同值」 │ └─ 物件 → Array.from({ length: n }, () => 新物件) - 每次迭代產生獨立實例 - 語意:「根據邏輯產生 n 個元素」 額外情況:每個元素的值依賴 index即使是原始值,若需要根據 index 計算,也要用 Array.from : delicious-insights// 產生 [0, 1, 2, 3, 4] Array.from({ length: 5 }, (_, i) => i) // 產生 [0, 3, 6, 9, 12](每人分配 3 個元素的起始位置) Array.from({ length: 5 }, (_, i) => i * 3) 快速查表情境建議寫法範例n 個相同原始值Array(n).fill(value)Array(5).fill(0)n 個獨立空陣列Array.from + callbackArray.from({ length: 5 }, () => [])n 個獨立空物件Array.from + callbackArray.from({ length: 5 }, () => ({}))根據 index 計算值Array.from + callbackArray.from({ length: 5 }, (_, i) => i * 2)五、分組計數演算法設計累加器模式(Accumulator Pattern)遍歷資料時用「累加到容器」的方式統計資料,這是最常見的計數模式:// 經典結構 const counts = {}; items.forEach((item) => { const key = generateKey(item); if (counts[key]) { counts[key] += 1; } else { counts[key] = 1; } }); Key 生成策略(Composite Key)當需要把「多欄位」或「陣列」轉成唯一的字串 key:// 多欄位組合 const key = `${item.name}-${item.spec}`; // 陣列排序後組合(順序無關的組合) const key = dancer.slice().sort().join('-'); 為什麼需要這樣? 因為 JS 物件的 key 只能是字串,Map 的 key 雖然可以是任意值,但陣列比較是 reference,不是 value 。所以都選擇「序列化成字串」來當 key。 developer.mozilla六、Map vs Object 完整對照API 語法對照表操作ObjectMap初始化const obj = {}const map = new Map()設定值obj[key] = valuemap.set(key, value)取值obj[key]map.get(key)檢查存在key in objmap.has(key)刪除delete obj[key]map.delete(key)大小Object.keys(obj).lengthmap.size取所有 valuesObject.values(obj)[...map.values()]遍歷Object.entries(obj).forEach(...)map.forEach((value, key) => ...)常見陷阱陷阱說明map[key] = value❌ 不會存進 Map,變成物件屬性key in obj會檢查原型鏈(如 'toString' in obj 是 true)map.has(key)只檢查你自己設的 key,更純粹 developer.mozilla// Object 原型鏈干擾 const obj = {}; 'toString' in obj; // true(原型鏈上的) // Map 乾淨 const map = new Map(); map.has('toString'); // false 效能比較根據實際測試,Map 在多數情況下有更好的效能表現 : zhenghao 記憶體使用:Map 比 Object 少 20-50% 記憶體使用 zhenghaoMap 不儲存屬性描述符(writable/enumerable/configurable)操作速度:字串 key:Map 在插入、刪除、迭代都較快 zhenghao數字 key:Object 在小範圍整數可能較快,但 Map 更穩定大量資料(>100000):Map 優勢明顯 zhenghao選擇時機情境建議value 是純數字/字串✅ Map 語意清晰value 是複雜物件兩者差異不大,選熟悉的key 是動態/使用者輸入✅ Map 較安全(避免 __proto__ 衝突)需要轉 JSON✅ Object(JSON.stringify(map) 會失敗)需要頻繁增刪 key✅ Map 效能較好 zhenghao需要保持插入順序✅ Map(語意更明確) developer.mozilla七、解構賦值(Destructuring Assignment)一句話定義從陣列或物件中「拆出」值,直接賦給變數,省去一個個取值的步驟。兩種解構方式類型匹配方式語法陣列解構靠「位置」const [a, b] = arr物件解構靠「屬性名稱」const { name, age } = obj陣列解構:位置對應// 傳統寫法 const pair = ['紅-藍', 3]; const key = pair[0]; const value = pair [jsdevspace.substack](https://jsdevspace.substack.com/p/how-to-master-recursion-in-javascript); // 解構寫法 const [key, value] = ['紅-藍', 3]; // key = '紅-藍'(位置 0) // value = 3(位置 1) 物件解構:名稱對應(順序無關)// 傳統寫法 const dancer = { name: '小明', color: '紅' }; const name = dancer.name; const color = dancer.color; // 解構寫法(順序可以不同) const { color, name } = { name: '小明', color: '紅' }; // name = '小明', color = '紅' 函式參數中解構最常用的情境,直接在參數位置拆值:// 陣列解構 const pairs = [['a', 1], ['b', 2]]; pairs.forEach(([key, value]) => { console.log(key, value); // 'a' 1, 'b' 2 }); // 物件解構 const people = [{ name: '小明', age: 20 }, { name: '小華', age: 25 }]; people.forEach(({ name, age }) => { console.log(name, age); // '小明' 20, '小華' 25 }); 實際應用:Map 轉陣列const map = new Map(); map.set('紅-藍', 3); map.set('綠-黃', 2); // map.entries() 展開後是 [['紅-藍', 3], ['綠-黃', 2]] // 用陣列解構把每個 [key, value] 拆開,轉成物件 const result = [...map.entries()].map(([key, value]) => ({ key, value })); // [{ key: '紅-藍', value: 3 }, { key: '綠-黃', value: 2 }] 八、JSDoc 型別定義:漸進式導入型別安全引入型別的好處提示(開發中):IDE 自動完成與參數提示 stefanjudis文件(開發後):註解即文件,易於維護檢查與拋錯(運行中):TypeScript 編譯時檢查JSDoc:用註解完成定義與得到提示JSDoc 的優勢在於導入成本相對低,因為寫在註解,程式運作不會壞掉,可以只寫其中一部分也沒關係 。 stefanjudis基礎型別定義語法使用 @typedef 定義自訂型別:/** * @typedef {object} User * @property {string} name - 使用者名稱 * @property {string} email - 使用者信箱 * @property {number=} age - 可選的年齡 */ /** @type {User} */ const user = { name: "Alex", email: "alex@example.com" }; 配合 Copilot 生成 JSDoc寫好函式或變數在上方輸入 /** 按 EnterCopilot 自動生成對應的 JSDoc 註解已經可以在開發過程得到足夠的正確型別提示 stefanjudisVS Code 型別檢查設定方式 1:單檔啟用// @ts-check const x = 123; x = "hello"; // ❌ Type 'string' is not assignable to type 'number' 方式 2:專案啟用(jsconfig.json) stefanjudis{ "compilerOptions": { "checkJs": true, "strict": true }, "exclude": ["node_modules", "**/node_modules/*"] } 方式 3:全域設定(settings.json) stefanjudis{ "javascript.implicitProjectConfig.checkJs": true } 進階:用單獨檔案管理型別定義Schemas.js(型別定義檔):/** * @typedef {object} User * @property {string} name - 使用者名稱 * @property {string} gender - 使用者性別 */ /** * @typedef {object} Product * @property {string} id - 產品編號 * @property {number} price - 產品價格 */ export const Types = {} main.js(使用型別):import { Types } from './Schemas.js'; /** @type {import('./Schemas.js').User} */ const user = { name: "Alex", gender: "male" }; // VS Code 會提示型別錯誤 user.age = 25; // ❌ Property 'age' does not exist 使用 ES Module 確保型別一致性以 ES module export/import 在開發過程確保宣告的變數符合事先定義的型別 : stefanjudis// types/schemas.js /** @typedef {object} Config */ export const ConfigType = {}; // utils.js /** @param {import('./types/schemas.js').Config} config */ export function init(config) { // 型別安全的操作 } 這樣即使不用 TypeScript,也能在純 JavaScript 專案中獲得型別提示與錯誤檢查,開發速度與穩定性兼顧 。 stefanjudis九、JavaScript 型別系統冷知識:JSFuck什麼是 JSFuck?JSFuck 是一種極限編程風格,只用 6 個字元 []()!+ 就能寫出任何 JavaScript 程式。// 正常寫法 alert(1) // JSFuck 寫法(部分) [][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]...][(...)...](...)() 為什麼可行?核心原理:JavaScript 的動態弱型別 + 隱性型別轉換 developer.mozilla任何程式都能轉成字串字串可以用 Function() 建構子執行六個符號透過型別轉換能產生所有需要的字元// 原理示範 Function('alert(1)')() // 等同 alert(1) 基礎型別轉換規則產生布林值![] // false - 空陣列轉布林後取反 !![] // true - 雙重否定 產生數字+[] // 0 - 空陣列轉數字 +!+[] // 1 - false 轉數字後取反再轉數字 !+[]+!+[] // 2 - 兩個 1 相加 產生特殊值[][[]] // undefined - 空陣列存取不存在的屬性 +[![]] // NaN - false 轉數字失敗 字串來源與可用字母表達式結果字串可取字母[]+{}"[object Object]"b c e j O o tfalse+[]"false"a e f l strue+[]"true"e r t u[][[]]+[]"undefined"d e f i n u+[![]]+[]"NaN"a N與 JS 型別系統的連結JSFuck 完美展示了以下概念 : developer.mozilla隱性強制轉型(Implicit Coercion):+ 運算子自動判斷要做字串連接或數字相加動態型別:同一變數可以是陣列、數字、字串,執行時才決定弱型別:自動轉換型別,不需手動宣告原型鏈:透過 constructor 存取建構函式Function 即物件:函式也是物件,有屬性可存取核心領悟: JSFuck 不是實用工具,而是 JavaScript 動態弱型別特性的極致藝術展示。它提醒我們:理解型別轉換規則很重要,否則程式行為會超出預期。延伸閱讀MDN 官方文件JavaScript 資料型別與資料結構 developer.mozillaArray.prototype.map developer.mozillaArray.prototype.fill developer.mozillaMap developer.mozilla最佳實踐JavaScript Recursion Best Practices jsdevspace.substackTDD in JavaScript Best Practices moldstudArray.from() vs Array.fill() delicious-insightsMap vs Object Performance zhenghaoVS Code JSDoc Type Checking stefanjudisECMAScript 規範ECMAScript 2026 Language Specification tc39Type Conversion tc39 ## 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