
你有沒有遇過這些狀況?
- API 呼叫太頻繁,想加一層 cache,但不想動 service 程式碼
- 表單送出前要驗證,但驗證邏輯散落在各個元件裡
- 某個物件初始化很貴(大圖、DB 連線),想等到真正需要時才建
這些全都是 Proxy Pattern 的主場。
先講結論
Proxy 就是在呼叫端和目標物件之間插一個「代理人」。代理人決定:要不要轉發請求、怎麼轉發、順便做什麼額外處理。重點是——原始物件完全不用改。
classDiagram class Subject { <<interface>> +display()* void } class RealImage { -filename : String +loadFromDisk() void +display() void } class ProxyImage { -filename : String -userRole : String -realImage : RealImage +display() void +hasAccess() boolean } Subject <|.. RealImage Subject <|.. ProxyImage ProxyImage --> RealImage : 延遲建立
經典範例:延遲載入 + 權限控制

class RealImage {
constructor(filename) {
this.filename = filename;
this.loadFromDisk(); // 建立時就載入,很貴
}
loadFromDisk() {
console.log(`Loading ${this.filename} from disk...`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
class ProxyImage {
constructor(filename, userRole = 'guest') {
this.filename = filename;
this.userRole = userRole;
this.realImage = null; // 還沒載入
}
display() {
if (this.userRole !== 'admin') {
console.log('Access denied.');
return;
}
if (!this.realImage) {
this.realImage = new RealImage(this.filename); // 第一次才載入
}
this.realImage.display();
}
}
const guest = new ProxyImage('photo.jpg', 'guest');
guest.display(); // Access denied — 連載入都省了
const admin = new ProxyImage('photo.jpg', 'admin');
admin.display(); // 第一次:載入 + 顯示
admin.display(); // 第二次:直接顯示,不重新載入ES6 Proxy:不用寫 class 的現代做法
ES6 原生的 Proxy 物件讓你攔截任何物件的操作,不用額外寫 class。這才是 JavaScript 開發者最常用的方式。
API Cache Proxy
const userService = {
fetchUser: async (id: string) => {
console.log(`[API] GET /users/${id}`);
return { id, name: 'Terry' };
},
};
const cache = new Map<string, { data: any; expiry: number }>();
const TTL = 60_000;
const cachedService = new Proxy(userService, {
get(target, prop: string) {
const original = target[prop];
if (typeof original !== 'function') return original;
return async (...args: any[]) => {
const key = `${prop}:${JSON.stringify(args)}`;
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache HIT] ${key}`);
return cached.data;
}
const result = await original.apply(target, args);
cache.set(key, { data: result, expiry: Date.now() + TTL });
return result;
};
},
});
await cachedService.fetchUser('123'); // [Cache MISS] → 打 API
await cachedService.fetchUser('123'); // [Cache HIT] → 不打 API原本的 userService 一行都沒改,但現在它有 cache 了。這就是 Proxy 的威力。
Validation Proxy
const validators = {
name: (v: any) => typeof v === 'string' && v.length > 0,
email: (v: any) => typeof v === 'string' && v.includes('@'),
age: (v: any) => typeof v === 'number' && v >= 0 && v <= 150,
};
const form = new Proxy({} as any, {
set(target, prop: string, value) {
const validate = validators[prop];
if (validate && !validate(value)) {
throw new Error(`Invalid value for ${prop}: ${value}`);
}
target[prop] = value;
return true;
},
});
form.name = 'Terry'; // OK
form.email = 'test@test.com'; // OK
// form.age = -5; // throws ErrorVue 3 怎麼用 Proxy?
Vue 3 的 reactive() 底層就是 ES6 Proxy。每次你讀一個屬性,它偷偷收集依賴(track);每次你寫一個屬性,它偷偷觸發更新(trigger)。這就是為什麼 Vue 3 不再需要 Vue.set()。
Proxy vs Decorator vs Adapter
| Proxy | Decorator | Adapter | |
|---|---|---|---|
| 目的 | 控制存取 | 擴充行為 | 轉換介面 |
| 介面 | 與原始物件相同 | 與原始物件相同 | 轉成新介面 |
| 常見場景 | cache、lazy load、ACL | logging、compression | 第三方 API 整合 |
搞混的話記這個口訣:Proxy 管「能不能」、Decorator 管「能做什麼」、Adapter 管「怎麼接」。
Proxy Pattern 就像大樓的保全——住戶(原始物件)不用變,但保全會幫你擋掉推銷員、代收包裹、順便記錄誰進誰出。只是保全太嚴格的話,連你自己都進不去。
延伸閱讀
- Decorator 模式 — 結構相似但目的不同,擴充行為而非控制存取
- Adapter 模式 — 介面轉換而非存取控制
- Mediator 模式 — 物件間複雜互動的協調
- Command 模式 — 把請求封裝成物件,常與 Proxy 搭配使用