JavaScript 的 Proxy 和 Reflect API 是該語言中最強大的元程式設計工具之一。它們允許開發人員攔截和自訂物件的基本操作——屬性存取、賦值、枚舉、函數調用,甚至建構函數調用。本文探討了所有 13 個代理陷阱、互補的 Reflect API 以及 Vue 3、MobX 和 Immer 等生產框架中使用的真實模式。
了解代理模式
Proxy 包裝目標物件並透過稱為陷阱的處理函數攔截操作:
const target = { name: "Alice", age: 30 };
const handler = {
get(target, prop, receiver) {
console.log(`Getting property "${prop}"`);
return Reflect.get(target, prop, receiver);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Getting property "name" → "Alice"
目標不變。處理程序定義如何轉送、修改或阻止代理程式上的操作。代理與包裝函數或裝飾器不同,因為它在語言層級進行攔截 - in、delete、for...in 和 new 都可以自訂。
13 代理陷阱
Proxy 提供了 13 個陷阱,涵蓋了每個基本的物件操作。最常用的是get和set,它們攔截屬性的讀寫。 以下是完整的參考:
| 陷阱 | 何時觸發 | 論點 |
|---|---|---|
| 佔位符_0 | 物業閱讀 | 佔位符_1 |
| 佔位符_0 | 屬性寫 | 佔位符_1 |
| 佔位符_0 | in 運算子 | 佔位符_2 |
| 佔位符_0 | delete 運算子 | 佔位符_2 |
| 佔位符_0 | Object.keys()、for...in | 佔位符_3 |
| 佔位符_0 | 函數呼叫 | 佔位符_1 |
| 佔位符_0 | new 關鍵字 | 佔位符_2 |
| 佔位符_0 | 佔位符_1 | 佔位符_2 |
| 佔位符_0 | 佔位符_1 | 佔位符_2 |
| 佔位符_0 | 佔位符_1 | 佔位符_2 |
| 佔位符_0 | 佔位符_1 | 佔位符_2 |
| 佔位符_0 | 佔位符_1 | 佔位符_2 |
| 佔位符_0 | 佔位符_1 | 佔位符_2 |
每個陷阱都必須強制執行不變量。例如,get 陷阱不能傳回與不可寫、不可配置屬性的實際值不同的值。
反射 API
Reflect 方法鏡像每個代理陷阱並提供預設轉送行為。在陷阱內使用 Reflect 可確保 receiver 參數的正確傳播,這對於繼承屬性和 getter 中正確的 this 綁定至關重要:
const parent = {
get fullName() { return `${this.first} ${this.last}`; }
};
const handler = {
get(target, prop, receiver) {
// Without Reflect.get, the getter's 'this' would be wrong
return Reflect.get(target, prop, receiver);
}
};
const proxy = new Proxy(Object.create(parent), handler);
proxy.first = "John";
proxy.last = "Doe";
console.log(proxy.fullName); // "John Doe"
思考模型很簡單:Reflect 與 Proxy 的關係就像 Object 與直接屬性存取一樣。始終透過 Reflect 轉送陷阱,以在新增自訂邏輯時保持預設行為。
驗證和資料完整性
set 陷阱可以對物件強制實施架構約束:
const validator = {
set(target, prop, value) {
if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
throw new TypeError("Age must be a positive integer");
}
if (prop === "email" && !/^[^\s@]+@[^\s@]+$/.test(value)) {
throw new TypeError("Invalid email format");
}
return Reflect.set(target, prop, value);
}
};
const user = new Proxy({}, validator);
user.age = 25; // OK
user.age = -5; // TypeError: Age must be a positive integer
只讀代理會阻止所有突變陷阱:
const readOnly = target =>
new Proxy(target, {
set: () => { throw new Error("Object is read-only"); },
deleteProperty: () => { throw new Error("Object is read-only"); },
defineProperty: () => { throw new Error("Object is read-only"); },
});
虛擬屬性和計算值
Proxy 可以透過在 get 陷阱中動態計算目標物件上不存在的屬性來公開這些屬性:
const range = (from, to) =>
new Proxy({ from, to }, {
get(target, prop) {
if (prop === "size") return target.to - target.from + 1;
if (prop === "contains") return (v) => v >= target.from && v <= target.to;
return Reflect.get(target, prop);
}
});
const r = range(1, 10);
console.log(r.size); // 10
console.log(r.contains(5)); // true
這種惰性求值模式可以透過 WeakMap 進行快取擴展,以避免重新計算。
財產觀察和變化跟踪
使用 set 陷阱的簡單可觀察:
function observe(target, onChange) {
return new Proxy(target, {
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
onChange(prop, value);
return result;
}
});
}
為了進行深度觀察,請使用 WeakMap 緩存遞歸地包裝嵌套對象,以避免雙重包裝。此技術是 Vue 3 的 reactive() 系統的基礎,它取代了 Vue 2 的基於 Object.defineProperty 的方法。
可撤銷代理
Proxy.revocable 建立一個可以永久停用的代理:
const { proxy, revoke } = Proxy.revocable(target, handler);
// use proxy normally...
revoke();
// any subsequent operation throws TypeError
這對於授予對敏感資源的臨時存取權限和執行清理合約非常有用。
生產模式:Vue 3、MobX、Immer
Vue 3 在其 reactive() 系統中使用 Proxy。 get 陷阱追蹤屬性存取以建立依賴關係圖,而 set 觸發重新渲染。 MobX 6+ 同樣將 defineProperty 替換為 observable() 的代理程式。 Immer.js 使用 Proxy 產生具有寫入時複製語意的不可變草稿 — set 標記修改,而 get 在存取時建立巢狀草稿。
性能考慮因素
每個被捕獲的操作都會產生開銷。避免在緊密循環中代理熱路徑物件。選擇性地應用代理程式——「代理內部,而不是外部」——並在優化之前進行分析。具有大型 WeakMap 快取的深度代理也會影響記憶體使用。
代理是驗證、反應性和虛擬資源的正確工具。對於效能關鍵路徑,請考慮更簡單的模式是否足夠。如果應用得當,Proxy 可以實現任何其他 JavaScript 功能不可能或繁瑣的元程式模式。

