很多人聽過「代理模式」,但到 JavaScript 裡常卡在「到底什麼時候該用 Proxy?」其實答案很生活化:想加日誌又不想改一堆檔、想把輸入驗證關到邊界、想讓昂貴資源延後建立,或希望對某個函式加上快取、重試與節流——這些都是 Proxy 擅長的場景。
本文不走炫技路線,而是用一系列小而完整的例子,示範 Proxy 如何在不影響原本 API 的前提下,加上你需要的控制點。你會看到每個 trap 的直覺用途、Reflect 的角色、何時用可撤銷代理、以及與 Decorator/Adapter/Facade 的差別。最後附上常見雷點清單與修正做法,幫你避開 this 綁定、陣列索引、不可寫屬性等細節坑洞。希望本篇文章可以幫助需要的您。
目錄
{tocify} $title={目錄}
代理(Proxy)設計模式是什麼?
Proxy(代理)是一種結構型設計模式:在客戶端與實際目標物件之間放入一個「替身」,攔截請求、加料或改道,再轉發給真實物件。
典型用途:
存取控制/權限管理(Protection Proxy)
惰性載入/延遲初始化(Virtual Proxy)
快取與去抖/節流(Cache/Rate-limit Proxy)
紀錄、監控、稽核(Logging/Audit Proxy)
失敗重試與容錯(Resiliency Proxy)
一句話總結:在不改變原物件程式碼的前提下,於「呼叫路徑」上插入可組態的控制點。
ES6 Proxy 與設計模式的對照關係
ES6 提供內建 new Proxy(target, handler),讓你以語言層級攔截屬性讀寫、函式呼叫、in、delete、new 等操作。
這正是實作「代理模式」的理想工具。
target:被代理的原物件(或函式)。
handler:一組攔截器(traps)方法,如 get、set、apply、construct…。
Reflect:與 Object 近似的標準工具箱,提供對應的原語操作(如 Reflect.get、Reflect.set),可用來安全地委派到原行為並遵守語言不變式。
設計模式層面的「代理」≠ 語言層面的 Proxy 物件,但兩者可自然對接:用 ES6 Proxy 來實作設計模式中的代理。
Handler 與 Reflect:常用 trap 快速圖鑑
| Trap | 觸發時機 | 常見用途 | 推薦委派 |
|---|---|---|---|
get(target, prop, receiver) |
物件屬性讀取 | 日誌、權限、惰性載入、響應式追蹤 | Reflect.get |
set(target, prop, value, receiver) |
物件屬性寫入 | 驗證、型別守衛、髒值標記 |
Reflect.set(回傳
true/false)
|
has(target, prop) |
prop in obj |
隱藏欄位、存取白名單 | Reflect.has |
deleteProperty(target, prop) |
delete obj[prop] |
禁刪/軟刪除 | Reflect |
ownKeys(target) |
Object.keys
|
欄位過濾、隱藏內部屬性 | Reflect.ownKeys |
getOwnPropertyDescriptor/ defineProperty
|
讀/設屬性描述 | 嚴格控制可列舉性、可寫性 | 對應 Reflect.* |
apply(target, thisArg, args) |
函式呼叫 | 記錄、快取、重試、限流 | Reflect.apply |
construct(target, args, newTarget) |
new 呼叫
|
建構流程加料、DI 注入 | Reflect.construct |
原則:在自訂邏輯後盡量用 Reflect.* 委派,既正確又可避免觸發語言不變式錯誤。
七種實務場景與可用範例
1. 觀測與記錄(Logging / Debug Proxy)
在不改動既有物件的情況下,追蹤誰讀了什麼、寫了什麼。
function createLoggerProxy(obj, name = 'obj') {
return new Proxy(obj, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
console.debug(`[GET] ${name}.${String(prop)} ->`, value);
return value;
},
set(target, prop, value, receiver) {
console.debug(`[SET] ${name}.${String(prop)} =`, value);
return Reflect.set(target, prop, value, receiver);
}
});
}
// 使用
const user = createLoggerProxy({ id: 1, role: 'editor' }, 'user');
user.role; // console: [GET] user.role -> "editor"
user.role = 'admin' // console: [SET] user.role = "admin"
要點:別忘了回傳 Reflect.set 的布林結果;嚴格模式下若回傳 false 會丟 TypeError。
2. 權限控制(Protection Proxy)
只允許特定角色讀寫特定欄位,否則拋錯或回傳遮罩值。
function authorizeProxy(obj, role, rules) {
// rules: { read: Set<string>, write: Set<string> }
return new Proxy(obj, {
get(target, prop, receiver) {
if (!rules.read.has(prop)) {
throw new Error(`角色 ${role} 無讀取欄位「${String(prop)}」的權限`);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (!rules.write.has(prop)) {
throw new Error(`角色 ${role} 無寫入欄位「${String(prop)}」的權限`);
}
return Reflect.set(target, prop, value, receiver);
},
ownKeys(target) {
// 列舉時只顯示允許讀取的欄位
return [...Reflect.ownKeys(target)].filter(k => rules.read.has(k));
}
});
}
// 使用
const profile = { id: 1, email: 'a@x.com', salary: 90000 };
const editorRules = { read: new Set(['id', 'email']), write: new Set(['email']) };
const editorView = authorizeProxy(profile, 'editor', editorRules);
console.log(Object.keys(editorView)); // ["id","email"]
editorView.salary; // Error: 無讀取權限
3. 惰性載入(Virtual Proxy)
直到需要時才初始化昂貴資源(例如大型資料或遠端連線)。
function lazyInitProxy(factory) {
let instance = null;
function ensure() {
if (!instance) instance = factory();
return instance;
}
return new Proxy({}, {
get(_, prop) {
return ensure()[prop];
},
set(_, prop, val) {
return Reflect.set(ensure(), prop, val);
},
has(_, prop) {
return Reflect.has(ensure(), prop);
}
});
}
// 使用
const db = lazyInitProxy(() => {
console.log('真正連線資料庫…(只會一次)');
return { query(sql){ return `run: ${sql}` } };
});
console.log('尚未觸發連線');
db.query('SELECT 1'); // 第一次讀取才建立
4. 函式快取(Memoization / Cache Proxy)
以 apply trap 快速為函式加上參數快取。
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
// 使用
const slowFib = n => n < 2 ? n : slowFib(n-1) + slowFib(n-2);
const fastFib = memoize(slowFib);
console.log(fastFib(40)); // 明顯快很多(相同輸入重複呼叫時)
生產環境請補上:快取大小上限、TTL、或改用 WeakMap 搭配物件型參數。
5. API 包裝:重試與節流(Resiliency / Rate Limit Proxy)
為 fetch 加上延遲、重試與退避,集中治理呼叫策略。
function resilientFetchProxy(fetchImpl = fetch, { retries = 2, baseDelay = 200 } = {}) {
return new Proxy(fetchImpl, {
async apply(target, thisArg, [url, options = {}]) {
let attempt = 0;
while (true) {
try {
const res = await Reflect.apply(target, thisArg, [url, options]);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (e) {
if (attempt++ >= retries) throw e;
const delay = baseDelay * (2 ** (attempt - 1));
await new Promise(r => setTimeout(r, delay));
}
}
}
});
}
// 使用
// const fetchSafe = resilientFetchProxy(fetch, { retries: 3, baseDelay: 250 });
// const resp = await fetchSafe('/api/data');
6. 寫入驗證(Validation Proxy)
集中欄位驗證,維持資料一致性。
function validatorProxy(obj, schema) {
// schema: { key: (value) => boolean }
return new Proxy(obj, {
set(target, prop, value, receiver) {
if (prop in schema && !schema[prop](value)) {
throw new TypeError(`欄位 ${String(prop)} 驗證失敗:${value}`);
}
return Reflect.set(target, prop, value, receiver);
}
});
}
// 使用
const article = validatorProxy({ title: '', tags: [] }, {
title: v => typeof v === 'string' && v.length >= 3,
tags: v => Array.isArray(v) && v.every(t => typeof t === 'string')
});
article.title = 'Proxy 模式指南'; // OK
article.tags = ['js', 'design']; // OK
// article.title = 1; // 丟 TypeError
7. 迷你「響應式」資料(Reactive-ish Proxy)
利用 get 收集依賴、set 觸發更新,示範原理(簡化版)。
function reactive(obj) {
const depsMap = new Map(); // prop -> Set<effect>
function track(prop) {
if (!currentEffect) return;
let set = depsMap.get(prop);
if (!set) depsMap.set(prop, (set = new Set()));
set.add(currentEffect);
}
function trigger(prop) {
const set = depsMap.get(prop);
if (set) for (const effect of set) effect();
}
let currentEffect = null;
function effect(fn) {
currentEffect = fn;
fn();
currentEffect = null;
}
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
track(prop);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const ok = Reflect.set(target, prop, value, receiver);
if (ok) trigger(prop);
return ok;
}
});
return { state: proxy, effect };
}
// 使用
const { state, effect } = reactive({ count: 0 });
effect(() => console.log('count ->', state.count));
state.count++; // 觸發 effect
與 Decorator/Adapter/Facade 的差異
Proxy:站在呼叫路徑上攔截,控制存取與行為(可無侵入地施作多種橫切關注)。
Decorator:包裹物件以增添功能,介面相同但邏輯更豐富。
Adapter:轉接介面,使不相容的 API 能互通。
Facade:提供統一簡化入口,隱藏系統複雜度,不攔截現有呼叫。
簡表:Proxy=門衛,Decorator=加值服務員,Adapter=轉接頭,Facade=櫃檯。
效能、相容性與記憶體考量
Proxy 會在每次讀寫時進入 trap,高頻路徑需注意開銷。
深層物件若層層包 Proxy,可能導致記憶體增長與調試複雜度。
相容性:現代瀏覽器與 Node.js 皆支援;若要支援非常舊環境(如 IE),需降級策略(但功能受限)。
可用 Proxy.revocable 產生可撤銷代理,避免長期引用導致資源外洩。
const { proxy, revoke } = Proxy.revocable({ x: 1 }, {});
proxy.x; // 1
revoke();
try { proxy.x } catch (e) { /* TypeError: Revoked */ }
常見錯誤與雷點
1. 忘了使用 Reflect 委派
風險:不遵守語言不變式(如不可寫屬性卻回報成功)。
✅ 作法:trap 內先做自訂邏輯,再呼叫對應 Reflect.*,把結果回傳。
2. set trap 回傳 false
嚴格模式下會丟 TypeError。
✅ 作法:除非要阻止寫入,否則回傳 Reflect.set(...) 的結果。
3. this 綁定跑到 Proxy 上
某些方法期望 this 指向 target,本體被換成 proxy 會壞。
✅ 作法:在 get 中對方法做 Function.prototype.bind 綁回 target,或乾脆不要攔截該方法。
get(target, prop, receiver) {
const v = Reflect.get(target, prop, receiver);
return typeof v === 'function' ? v.bind(target) : v;
}
4. instanceof 與身份判等差異
proxy !== target,某些第三方庫做嚴格比對會失敗。
✅ 作法:盡量把 proxy 的外露面縮小在邏輯邊界;必要時在 getPrototypeOf/has 等 trap 上維持行為一致。
5. 陣列索引與長度陷阱
攔截 set 沒處理好,length、稀疏陣列行為可能異常。
✅ 作法:對陣列使用 proxy 時盡量透明委派,少做「魔改」。
6. 不可配置或唯讀屬性的不變式
例如 target 有 configurable:false 且 writable:false 的屬性,get 必須回傳相同值。
✅ 作法:在 get 先讀 Reflect.getOwnPropertyDescriptor 檢查必要不變式。
7. JSON.stringify、序列化與列舉
ownKeys/getOwnPropertyDescriptor 影響可列舉與序列化結果。
✅ 作法:如非必要,讓這些 trap 直接 Reflect.* 委派。
8. 建立大量短命 Proxy 導致 GC 壓力
✅ 作法:以工廠+快取管理代理(如 WeakMap<target, proxy>)。
9. 私有欄位(#private)不是一般屬性
Proxy 無法攔截 Class 的 #private 存取。
✅ 作法:改以公開 API 攔截,或改走方法代理而非直接欄位。
10. 循環依賴/再入陷阱
在 trap 裡又觸發會進入同一個 trap。
✅ 作法:注意使用暫停旗標或以 Reflect 直接委派避免重入。
問題集
Q1:JSON.stringify(proxy) 跟 target 一樣嗎?
A:若未改 ownKeys/getOwnPropertyDescriptor,通常一致;否則結果可能不同。保持透明委派最保險。
Q2:為什麼會出現 TypeError: 'set' on proxy returned false?
A:嚴格模式下 set 回傳 false 代表寫入失敗即拋錯。回傳 Reflect.set(...)。
Q3:如何只攔截函式呼叫?
A:把 函式 當 target,使用 apply(與 construct)trap:
const fn = (a, b) => a + b;
const proxied = new Proxy(fn, {
apply(target, thisArg, args) {
console.log('call with', args);
return Reflect.apply(target, thisArg, args);
}
});
proxied(1, 2);
Q4:Class 的 #private 欄位能攔嗎?
A:不能。那是語法層級的私有,不經由一般屬性路徑。
Q5:和 Decorator(裝飾者)有什麼不同?我也能用 decorator 語法嗎?
A:概念不同。Decorator 通常是包裝行為並回傳新物件或修改定義;Proxy 是攔截存取。JS 的 Stage 裝飾器屬於不同議題,別混為一談。
綜合範例:可拔插的多重代理管線
把「日誌+驗證+快取」組成可重用管線,僅在模組邊界輸出代理。
// 1) 核心服務(勿動)
const Service = {
add(a, b) { return a + b; },
find(id) { return { id, name: `item-${id}` }; }
};
// 2) 日誌代理
const withLog = obj => new Proxy(obj, {
get(t, p, r) {
const v = Reflect.get(t, p, r);
if (typeof v === 'function') {
return function(...args) {
console.time(`log:${String(p)}`);
const out = v.apply(this, args);
console.timeEnd(`log:${String(p)}`);
return out;
}
}
return v;
}
});
// 3) 參數驗證代理
const withValidation = obj => new Proxy(obj, {
get(t, p, r) {
const v = Reflect.get(t, p, r);
if (p === 'add') {
return (a, b) => {
if (![a,b].every(Number.isFinite)) throw new TypeError('add 需數字');
return v(a, b);
}
}
return v;
}
});
// 4) 回傳快取代理(僅示範 find)
const withCache = obj => {
const cache = new Map();
return new Proxy(obj, {
get(t, p, r) {
const v = Reflect.get(t, p, r);
if (p === 'find') {
return (id) => {
if (cache.has(id)) return cache.get(id);
const res = v(id);
cache.set(id, res);
return res;
}
}
return v;
}
});
};
// 5) 組管線
const service = withCache(withValidation(withLog(Service)));
// 使用
service.add(1, 2);
service.find(10);
service.find(10); // 命中快取
SEO 與可維護性角度的小提醒
文字內容避免關鍵字堆疊,但可自然重申:Proxy 模式、JavaScript Proxy、ES6 Proxy 實作、代理設計模式。
程式片段採可複製、可執行的最小示例;命名清楚(withXxx)。
以情境段落標題切分:讀者更容易從搜尋結果進來就定位到解法。
結語
Proxy 把「攔截」變成一等公民:你可以在不改動核心邏輯的前提下,加入日誌、權限、惰性載入、快取、重試與驗證等能力。
關鍵只在兩點:透明委派(Reflect)與邊界治理(只在對外輸出處做代理)。掌握這兩招,你的 JavaScript 代理就夠穩、好測、可維護。
延伸閱讀推薦:
javaScript設計模式 : Factory Method (工廠方法)
javaScript設計模式 : Abstract Factory(抽象工廠)
javaScript設計模式 : Builder(建造者模式)
javaScript設計模式 : Prototype(原型)
javaScript設計模式 : Singleton (單例模式)
javaScript設計模式 : Adapter(轉接器模式)
javaScript設計模式 : Bridge( 橋接模式 )
javaScript設計模式 : Composite(組合模式)
javaScript設計模式 : Decorator(裝飾者)
javaScript設計模式 : Chain of Responsibility(責任鏈)
javaScript設計模式 : Command Pattern(命令模式)
javaScript設計模式 : Interpreter(直譯器)
javaScript設計模式 : Iterator(迭代器)
javaScript設計模式 : Mediator(仲裁者)
javaScript設計模式 : Observer( 觀察者 )
javaScript設計模式 : Strategy(策略模式)
