JavaScript Proxy 和 Reflect:元程式設計的強大利器
6 分鐘閱讀 1,000 字
JavaScript Proxy 和 Reflect:元程式設計的強大利器
Proxy 和 Reflect 是 ES6 引入的兩個強大 API,讓你能夠攔截並自訂幾乎所有的物件操作。雖然日常開發中不常直接使用它們,但許多流行的框架和庫(Vue 3 的響應式系統、Immer.js、MobX)都在底層大量使用這兩個 API。
Proxy 基本概念
Proxy 讓你建立一個「代理」物件,攔截對目標物件的各種操作:
const target = { name: "Alice", age: 25 };
const handler = {
get(target, prop, receiver) {
console.log(`讀取屬性:${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`設定屬性:${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy(target, handler);
proxy.name; // 輸出:讀取屬性:name
proxy.age = 26; // 輸出:設定屬性:age = 26可攔截的操作(Trap)
Proxy 支援 13 種 trap,涵蓋所有物件操作:
| Trap | 攔截的操作 |
|---|---|
| `get` | 讀取屬性 `obj.prop` |
| `set` | 設定屬性 `obj.prop = val` |
| `has` | `in` 運算子 |
| `deleteProperty` | `delete obj.prop` |
| `apply` | 函式呼叫 |
| `construct` | `new` 運算子 |
| `getOwnPropertyDescriptor` | `Object.getOwnPropertyDescriptor()` |
| `ownKeys` | `Object.keys()`, `for...in` |
| `defineProperty` | `Object.defineProperty()` |
| `getPrototypeOf` | `Object.getPrototypeOf()` |
| `setPrototypeOf` | `Object.setPrototypeOf()` |
| `isExtensible` | `Object.isExtensible()` |
| `preventExtensions` | `Object.preventExtensions()` |
實用範例一:資料驗證
function createValidatedObject(schema) {
return new Proxy({}, {
set(target, prop, value) {
if (prop in schema) {
const validator = schema[prop];
if (!validator(value)) {
throw new TypeError(
`屬性 "${prop}" 的值 "${value}" 不符合驗證規則`
);
}
}
target[prop] = value;
return true;
}
});
}
const user = createValidatedObject({
name: (v) => typeof v === 'string' && v.length > 0,
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
});
user.name = "Bob"; // OK
user.age = 25; // OK
user.email = "not-an-email"; // 拋出 TypeError實用範例二:不可變物件(類似 Immer)
function makeImmutable(obj) {
return new Proxy(obj, {
set(target, prop, value) {
throw new TypeError(`不能修改屬性 "${prop}",物件是不可變的`);
},
deleteProperty(target, prop) {
throw new TypeError(`不能刪除屬性 "${prop}",物件是不可變的`);
},
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
// 遞迴使嵌套物件也不可變
if (typeof value === 'object' && value !== null) {
return makeImmutable(value);
}
return value;
}
});
}
const config = makeImmutable({
db: { host: 'localhost', port: 5432 },
cache: { ttl: 3600 }
});
config.db.host = 'other'; // TypeError實用範例三:自動預設值
// 存取不存在的屬性時,回傳預設值或自動建立
function withDefaults(target, defaults = {}) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
if (prop in defaults) {
return defaults[prop];
}
// 未定義的屬性回傳 null 而非 undefined
return null;
}
});
}
const settings = withDefaults(
{ theme: 'dark' },
{ language: 'zh-TW', timezone: 'Asia/Taipei', notifications: true }
);
console.log(settings.theme); // 'dark'
console.log(settings.language); // 'zh-TW'(來自 defaults)
console.log(settings.unknown); // null實用範例四:追蹤物件變更
function trackChanges(obj) {
const changes = [];
const proxy = new Proxy(obj, {
set(target, prop, value, receiver) {
changes.push({
type: 'set',
prop,
oldValue: target[prop],
newValue: value,
timestamp: Date.now()
});
return Reflect.set(target, prop, value, receiver);
},
deleteProperty(target, prop) {
changes.push({
type: 'delete',
prop,
oldValue: target[prop],
timestamp: Date.now()
});
return Reflect.deleteProperty(target, prop);
}
});
return { proxy, getChanges: () => [...changes] };
}
const { proxy: user, getChanges } = trackChanges({ name: 'Alice', age: 25 });
user.name = 'Alicia';
user.age = 26;
delete user.age;
console.log(getChanges());
// [
// { type: 'set', prop: 'name', oldValue: 'Alice', newValue: 'Alicia', ... },
// { type: 'set', prop: 'age', oldValue: 25, newValue: 26, ... },
// { type: 'delete', prop: 'age', oldValue: 26, ... }
// ]Reflect API
Reflect 是 Proxy 的最佳夥伴,提供與 Proxy trap 一一對應的靜態方法:
// 為什麼要用 Reflect 而非直接操作?
// 問題一:this 綁定錯誤
const handler = {
get(target, prop, receiver) {
// 錯誤:直接存取可能破壞繼承鏈上的 getter
// return target[prop];
// 正確:Reflect.get 保留正確的 receiver
return Reflect.get(target, prop, receiver);
}
};
// 問題二:set 必須回傳 boolean
const handler2 = {
set(target, prop, value, receiver) {
// 錯誤:忘記回傳 true 會在 strict mode 拋出 TypeError
// target[prop] = value;
// 正確:Reflect.set 回傳 boolean
return Reflect.set(target, prop, value, receiver);
}
};攔截函式呼叫(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)) {
console.log(`快取命中:${key}`);
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
const expensiveCalc = memoize((n) => {
console.log(`計算中...`);
return n * n;
});
expensiveCalc(5); // 計算中... → 25
expensiveCalc(5); // 快取命中 → 25
expensiveCalc(6); // 計算中... → 36效能考量
Proxy 比直接物件操作慢,使用前要評估:
- 讀寫頻繁的熱路徑(hot path)應避免使用
- 建立大量 Proxy 實例會增加 GC 壓力
- 在效能敏感場景,考慮只在開發環境啟用 Proxy(如驗證邏輯)
小結
Proxy 和 Reflect 開啟了 JavaScript 元程式設計的大門。資料驗證、響應式系統、不可變資料結構、ORM 映射——許多複雜的功能都能用這兩個 API 優雅地實現。Vue 3 的整個響應式系統就建立在 Proxy 之上,理解它們能讓你更深刻地理解現代框架的運作原理。
分享這篇文章