
Proxy 模式:物件訪問的守門員
你有沒有遇過這些情境?
- 一個 API 呼叫太頻繁,你想在前端加一層 cache,但不想動到現有的 service 程式碼
- 表單送出前需要驗證欄位,但驗證邏輯散落在各個元件裡,難以統一管理
- 某個物件初始化很昂貴(例如載入大圖、建立 DB 連線),你想等到真正需要時才建立
這些都是 Proxy 模式的典型應用場景。它的核心概念很簡單:在呼叫端和目標物件之間插入一個「代理人」,讓代理人決定要不要轉發請求、怎麼轉發、以及做什麼額外處理。
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 : 延遲建立
模式設計
Proxy 模式的核心思想是創建一個代理物件,這個代理物件控制著對原始物件的訪問,可以在訪問前後執行額外的邏輯(例如權限驗證、快取、延遲載入等)。
使用情境
-
遠端代理:
- 例如 RPC/GraphQL client,代理物件負責封裝網路呼叫細節。
-
虛擬代理:
- 例如圖片、影片縮圖延遲載入,在真正需要時才建立大型資源。
-
保護代理:
- 例如後台管理頁面、檔案下載權限,代理物件可先檢查權限再轉交。
實作範例

// RealImage.js - 真實圖像類別(Real Subject)
class RealImage {
constructor(filename) {
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`Loading ${this.filename} from disk...`);
}
display() {
console.log(`Displaying ${this.filename}`);
}
}
// ProxyImage.js - 代理圖像類別(Proxy)
class ProxyImage {
constructor(filename, userRole = 'guest') {
this.filename = filename;
this.userRole = userRole;
this.realImage = null;
}
display() {
if (!this.hasAccess()) {
console.log('Access denied.');
return;
}
if (!this.realImage) {
this.realImage = new RealImage(this.filename); // 延遲載入
}
this.realImage.display();
}
hasAccess() {
return this.userRole === 'admin';
}
}
// 使用範例
const guestImage = new ProxyImage('photo.jpg', 'guest');
guestImage.display(); // 權限不足
const adminImage = new ProxyImage('photo.jpg', 'admin');
adminImage.display(); // 第一次會載入
adminImage.display(); // 第二次直接顯示現代 JavaScript:ES6 Proxy
ES6 原生提供了 Proxy 物件,讓你不用寫 class 就能實作 Proxy 模式。這是 JavaScript 中最常用的 Proxy 實作方式。
API Cache Proxy
// 問題:每次呼叫 fetchUser 都打一次 API
// 解法:用 Proxy 自動加 cache 層,不改原本的 service 程式碼
interface UserService {
fetchUser: (id: string) => Promise<{ id: string; name: string }>;
fetchOrders: (userId: string) => Promise<any[]>;
}
const userService: UserService = {
fetchUser: async (id) => {
console.log(`[API] GET /users/${id}`);
return { id, name: 'Terry' };
},
fetchOrders: async (userId) => {
console.log(`[API] GET /users/${userId}/orders`);
return [{ orderId: '001', total: 1500 }];
},
};
// 用 Proxy 幫 service 加 cache,原本的 service 完全不用改
const cache = new Map<string, { data: any; expiry: number }>();
const TTL = 60_000; // 1 分鐘
const cachedService = new Proxy(userService, {
get(target, prop: keyof UserService) {
const original = target[prop];
if (typeof original !== 'function') return original;
return async (...args: any[]) => {
const key = `${String(prop)}:${JSON.stringify(args)}`;
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache HIT] ${key}`);
return cached.data;
}
console.log(`[Cache MISS] ${key}`);
const result = await (original as Function).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] → 不打 APIValidation Proxy
// 在物件被賦值時自動驗證,不需要在每個 setter 裡寫驗證邏輯
interface UserForm {
name: string;
email: string;
age: number;
}
const validators: Record<keyof UserForm, (val: any) => boolean> = {
name: (v) => typeof v === 'string' && v.length > 0,
email: (v) => typeof v === 'string' && v.includes('@'),
age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
};
const form = new Proxy<UserForm>({} as UserForm, {
set(target, prop: keyof UserForm, value) {
const validate = validators[prop];
if (validate && !validate(value)) {
throw new Error(`Invalid value for ${String(prop)}: ${value}`);
}
target[prop] = value;
return true;
},
});
form.name = 'Terry'; // OK
form.email = 'test@test.com'; // OK
// form.age = -5; // throws Error: Invalid value for age: -5真實世界的 Proxy
Vue 3 的 Reactive System
Vue 3 放棄了 Vue 2 的 Object.defineProperty,改用 ES6 Proxy 實作響應式系統。這是 Proxy 模式最知名的大規模應用之一:
// Vue 3 的 reactive() 簡化版原理
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key); // 收集依賴(誰在讀這個值)
return Reflect.get(obj, key, receiver);
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver);
trigger(obj, key); // 觸發更新(通知所有依賴方)
return result;
},
});
}其他知名開源專案
- MobX:用 Proxy 追蹤 observable state 的存取
- Immer:用 Proxy 實現 immutable state 的 draft 機制(寫起來像 mutable,結果是 immutable)
- Express middleware:
req/res物件經過中介層處理,本質就是 Proxy 概念
什麼時候該用 / 什麼時候不該用
該用的訊號
- 你想在不改動原始物件/service的前提下,加入 cache、log、權限、驗證
- 物件建立成本高,需要延遲初始化(lazy loading)
- 你需要一個統一的切入點來做存取控制
不該用的訊號
- 邏輯簡單到一個
if就能解決 — 不需要多包一層 - 你打算改變物件的功能而非控制存取 — 考慮 Decorator 模式
- 物件間的互動很複雜 — 考慮 Mediator 模式
常見誤用
- 把 Proxy 當 Decorator 用:Proxy 是控制「能不能存取」,Decorator 是擴充「能做什麼」。如果你的 Proxy 只是在加功能而不是在做存取控制,那你用錯 pattern 了。
- Proxy 層太多:A proxy B proxy C proxy D… 除錯時會很痛苦。通常一層就夠了。
- 忘記處理
thisbinding:ES6 Proxy 的gettrap 需要用Reflect.get來正確處理this,直接target[prop]可能會壞掉。
優點
- 可以在不改變原始物件的情況下控制訪問
- 支援延遲載入,提高效能
- 可集中處理權限、快取、紀錄等邏輯
- ES6 Proxy 讓實作更簡潔,不需要寫 class
缺點
- 增加系統複雜度
- 可能降低響應速度(若代理邏輯過重)
- ES6 Proxy 無法被 polyfill(不支援 IE11)
- 過度使用會讓 debug 變困難(stack trace 變深)
Proxy vs Decorator vs Adapter
| 比較 | Proxy | Decorator | Adapter |
|---|---|---|---|
| 目的 | 控制存取 | 擴充行為 | 轉換介面 |
| 介面 | 與原始物件相同 | 與原始物件相同 | 轉成新介面 |
| 關注點 | 誰能存取、何時存取 | 加什麼功能 | 怎麼讓不相容的介面合作 |
| 常見場景 | cache、lazy load、ACL | logging、compression | 第三方 API 整合 |
下次遇到這些情境,試試 Proxy
- 想幫 API service 加 cache → API Cache Proxy
- 想統一表單驗證邏輯 → Validation Proxy
- 想延遲載入大型資源 → Virtual Proxy
- 想加存取權限控制 → Protection Proxy
延伸閱讀
- Decorator 模式 — 結構相似但目的不同,擴充行為而非控制存取
- Adapter 模式 — 介面轉換而非存取控制
- Mediator 模式 — 物件間複雜互動的協調
- Command 模式 — 把請求封裝成物件,常與 Proxy 搭配使用