線上課程觀課進度管理小工具開發日誌
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」。
整理日期: 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.plainenglish
如果需要動態執行字串運算,可使用 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}`;
}
所有程式碼都應該用相同的思考流程:看到需求時,先定義清楚輸入(Input)與輸出(Output),實作不通再回頭調整計畫 。 moldstud
TDD 強制先寫測試的本質,就是訓練「實作前先想清楚」的思維模式 。 moldstud
何時該抽出共用函式?
回到自然語言判斷「這是一件獨立的事」,而非單純看到重複程式碼就抽出來 。 moldstud
正確思維: 「我要取奇數索引值的元素」是一件獨立且完整的事,後續被重複使用只是結果
錯誤思維: 看到兩處程式碼一樣就立刻抽成函式
程式碼的表達方式要考慮閱讀者感受,所以對「單獨一件事」的定義很重要。
實作 Caesar Cipher 加密函式,將輸入字串的英文字母(a-z, A-Z)依指定位移量平移,其他字元保持原樣 。 caesar-cipher
需求:
支援小寫、大寫字母
處理字母包回(wrap around),如 xyz → abc
非字母字元(數字、標點、空白)原樣回傳
產生小寫字母陣列 lowerAlphabet(a-z)與大寫字母陣列 upperAlphabet(A-Z)
對每個字元判斷:
若是小寫 → 位移加密並處理包回
若是大寫 → 位移加密並處理包回
其他字元 → 原樣回傳
使用 Array.prototype.map 處理字元陣列,最後用 join('') 組回字串 caesar-cipher
Array.from:產生字母陣列,搭配 String.fromCharCode 建立 Unicode 字元 delicious-insights
split(''):將字串拆成字元陣列
map:逐字元處理加密邏輯 developer.mozilla
indexOf:判斷字元是否為英文字母並取得索引
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): 讓程式更易讀、易維護
分類處理: 支援多種輸入時,先分類再處理,避免複雜的條件判斷
string、number、boolean、null、undefined、symbol、bigint
賦值時複製值本身
每個變數獨立,互不影響
array、object、function
賦值時複製參考(記憶體位址)
多個變數可能指向同一物件,修改會互相影響 developer.mozilla
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"]] ← 全部都被改了!
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 計算,也要用 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 個相同原始值 |
|
|
n 個獨立空陣列 |
|
|
n 個獨立空物件 |
|
|
根據 index 計算值 |
|
|
遍歷資料時用「累加到容器」的方式統計資料,這是最常見的計數模式:
// 經典結構
const counts = {};
items.forEach((item) => {
const key = generateKey(item);
if (counts[key]) {
counts[key] += 1;
} else {
counts[key] = 1;
}
});
當需要把「多欄位」或「陣列」轉成唯一的字串 key:
// 多欄位組合
const key = `${item.name}-${item.spec}`;
// 陣列排序後組合(順序無關的組合)
const key = dancer.slice().sort().join('-');
為什麼需要這樣?
因為 JS 物件的 key 只能是字串,Map 的 key 雖然可以是任意值,但陣列比較是 reference,不是 value 。所以都選擇「序列化成字串」來當 key。 developer.mozilla
操作 | Object | Map |
|---|---|---|
初始化 |
|
|
設定值 |
|
|
取值 |
|
|
檢查存在 |
|
|
刪除 |
|
|
大小 |
|
|
取所有 values |
陷阱 | 說明 |
|---|---|
| 不會存進 Map,變成物件屬性 |
| 會檢查原型鏈(如 |
| 只檢查你自己設的 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% 記憶體使用 zhenghao
Map 不儲存屬性描述符(writable/enumerable/configurable)
操作速度:
情境 | 建議 |
|---|---|
value 是純數字/字串 | Map 語意清晰 |
value 是複雜物件 | 兩者差異不大,選熟悉的 |
key 是動態/使用者輸入 | Map 較安全(避免 |
需要轉 JSON | Object( |
需要頻繁增刪 key | Map 效能較好 zhenghao |
需要保持插入順序 | Map(語意更明確) developer.mozilla |
從陣列或物件中「拆出」值,直接賦給變數,省去一個個取值的步驟。
類型 | 匹配方式 | 語法 |
|---|---|---|
陣列解構 | 靠「位置」 |
|
物件解構 | 靠「屬性名稱」 |
|
// 傳統寫法
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
});
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 }]
提示(開發中):IDE 自動完成與參數提示 stefanjudis
文件(開發後):註解即文件,易於維護
檢查與拋錯(運行中):TypeScript 編譯時檢查
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"
};
寫好函式或變數
在上方輸入 /** 按 Enter
Copilot 自動生成對應的 JSDoc 註解
已經可以在開發過程得到足夠的正確型別提示 stefanjudis
方式 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 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
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 轉數字失敗
表達式 | 結果字串 | 可取字母 |
|---|---|---|
|
| b c e j O o t |
|
| a e f l s |
|
| e r t u |
|
| d e f i n u |
|
| a N |
JSFuck 完美展示了以下概念 : developer.mozilla
隱性強制轉型(Implicit Coercion):+ 運算子自動判斷要做字串連接或數字相加
動態型別:同一變數可以是陣列、數字、字串,執行時才決定
弱型別:自動轉換型別,不需手動宣告
原型鏈:透過 constructor 存取建構函式
Function 即物件:函式也是物件,有屬性可存取
核心領悟: JSFuck 不是實用工具,而是 JavaScript 動態弱型別特性的極致藝術展示。它提醒我們:理解型別轉換規則很重要,否則程式行為會超出預期。
整理日期: 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.plainenglish
如果需要動態執行字串運算,可使用 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}`;
}
所有程式碼都應該用相同的思考流程:看到需求時,先定義清楚輸入(Input)與輸出(Output),實作不通再回頭調整計畫 。 moldstud
TDD 強制先寫測試的本質,就是訓練「實作前先想清楚」的思維模式 。 moldstud
何時該抽出共用函式?
回到自然語言判斷「這是一件獨立的事」,而非單純看到重複程式碼就抽出來 。 moldstud
正確思維: 「我要取奇數索引值的元素」是一件獨立且完整的事,後續被重複使用只是結果
錯誤思維: 看到兩處程式碼一樣就立刻抽成函式
程式碼的表達方式要考慮閱讀者感受,所以對「單獨一件事」的定義很重要。
實作 Caesar Cipher 加密函式,將輸入字串的英文字母(a-z, A-Z)依指定位移量平移,其他字元保持原樣 。 caesar-cipher
需求:
支援小寫、大寫字母
處理字母包回(wrap around),如 xyz → abc
非字母字元(數字、標點、空白)原樣回傳
產生小寫字母陣列 lowerAlphabet(a-z)與大寫字母陣列 upperAlphabet(A-Z)
對每個字元判斷:
若是小寫 → 位移加密並處理包回
若是大寫 → 位移加密並處理包回
其他字元 → 原樣回傳
使用 Array.prototype.map 處理字元陣列,最後用 join('') 組回字串 caesar-cipher
Array.from:產生字母陣列,搭配 String.fromCharCode 建立 Unicode 字元 delicious-insights
split(''):將字串拆成字元陣列
map:逐字元處理加密邏輯 developer.mozilla
indexOf:判斷字元是否為英文字母並取得索引
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): 讓程式更易讀、易維護
分類處理: 支援多種輸入時,先分類再處理,避免複雜的條件判斷
string、number、boolean、null、undefined、symbol、bigint
賦值時複製值本身
每個變數獨立,互不影響
array、object、function
賦值時複製參考(記憶體位址)
多個變數可能指向同一物件,修改會互相影響 developer.mozilla
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"]] ← 全部都被改了!
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 計算,也要用 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 個相同原始值 |
|
|
n 個獨立空陣列 |
|
|
n 個獨立空物件 |
|
|
根據 index 計算值 |
|
|
遍歷資料時用「累加到容器」的方式統計資料,這是最常見的計數模式:
// 經典結構
const counts = {};
items.forEach((item) => {
const key = generateKey(item);
if (counts[key]) {
counts[key] += 1;
} else {
counts[key] = 1;
}
});
當需要把「多欄位」或「陣列」轉成唯一的字串 key:
// 多欄位組合
const key = `${item.name}-${item.spec}`;
// 陣列排序後組合(順序無關的組合)
const key = dancer.slice().sort().join('-');
為什麼需要這樣?
因為 JS 物件的 key 只能是字串,Map 的 key 雖然可以是任意值,但陣列比較是 reference,不是 value 。所以都選擇「序列化成字串」來當 key。 developer.mozilla
操作 | Object | Map |
|---|---|---|
初始化 |
|
|
設定值 |
|
|
取值 |
|
|
檢查存在 |
|
|
刪除 |
|
|
大小 |
|
|
取所有 values |
陷阱 | 說明 |
|---|---|
| 不會存進 Map,變成物件屬性 |
| 會檢查原型鏈(如 |
| 只檢查你自己設的 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% 記憶體使用 zhenghao
Map 不儲存屬性描述符(writable/enumerable/configurable)
操作速度:
情境 | 建議 |
|---|---|
value 是純數字/字串 | Map 語意清晰 |
value 是複雜物件 | 兩者差異不大,選熟悉的 |
key 是動態/使用者輸入 | Map 較安全(避免 |
需要轉 JSON | Object( |
需要頻繁增刪 key | Map 效能較好 zhenghao |
需要保持插入順序 | Map(語意更明確) developer.mozilla |
從陣列或物件中「拆出」值,直接賦給變數,省去一個個取值的步驟。
類型 | 匹配方式 | 語法 |
|---|---|---|
陣列解構 | 靠「位置」 |
|
物件解構 | 靠「屬性名稱」 |
|
// 傳統寫法
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
});
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 }]
提示(開發中):IDE 自動完成與參數提示 stefanjudis
文件(開發後):註解即文件,易於維護
檢查與拋錯(運行中):TypeScript 編譯時檢查
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"
};
寫好函式或變數
在上方輸入 /** 按 Enter
Copilot 自動生成對應的 JSDoc 註解
已經可以在開發過程得到足夠的正確型別提示 stefanjudis
方式 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 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
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 轉數字失敗
表達式 | 結果字串 | 可取字母 |
|---|---|---|
|
| b c e j O o t |
|
| a e f l s |
|
| e r t u |
|
| d e f i n u |
|
| a N |
JSFuck 完美展示了以下概念 : developer.mozilla
隱性強制轉型(Implicit Coercion):+ 運算子自動判斷要做字串連接或數字相加
動態型別:同一變數可以是陣列、數字、字串,執行時才決定
弱型別:自動轉換型別,不需手動宣告
原型鏈:透過 constructor 存取建構函式
Function 即物件:函式也是物件,有屬性可存取
核心領悟: JSFuck 不是實用工具,而是 JavaScript 動態弱型別特性的極致藝術展示。它提醒我們:理解型別轉換規則很重要,否則程式行為會超出預期。
|
遍歷 |
|
|
|
遍歷 |
|
|
Share Dialog
Share Dialog
No comments yet