【每日一面】你怎么理解 Proxy 的
基礎問答
問:Proxy 是什么?怎么使用的?
答:Proxy 是用于創建 “對象代理” 的構造函數,它能封裝目標對象(target),并通過 “攔截器對象(handler)” 自定義目標對象的基礎操作(如屬性讀取、賦值),實現對對象行為的 “劫持”,手寫使用方式。
// 語法:new Proxy(target, handler)
// 參數:target-目標對象,handler-攔截器對象
// 返回值:代理實例proxy
const target = { name: '前端面試', age: 2 };
// 定義攔截器對象
const handler = {
// 攔截“讀取屬性”操作:參數為target(目標對象)、prop(屬性名)、receiver(代理實例)
get(target, prop, receiver) {
console.log(`觸發get攔截:讀取屬性${prop}`);
// 執行原始讀取操作(通過Reflect確保this指向正確)
return Reflect.get(target, prop, receiver);
},
// 攔截“賦值屬性”操作:參數為target、prop、value(新值)、receiver
set(target, prop, value, receiver) {
console.log(`觸發set攔截:給屬性${prop}賦值${value}`);
// 自定義邏輯:校驗age屬性必須為數字
if (prop === 'age' && typeof value !== 'number') {
throw new Error('age屬性必須是數字');
}
// 執行原始賦值操作
return Reflect.set(target, prop, value, receiver);
}
};
// 創建代理實例
const proxy = new Proxy(target, handler);
// 操作代理實例,觸發攔截
console.log(proxy.name); // 輸出:觸發get攔截:讀取屬性name → 前端面試
proxy.age = 3; // 輸出:觸發set攔截:給屬性age賦值3 → 成功
proxy.age = '3'; // 輸出:觸發set攔截:給屬性age賦值3 → 拋出錯誤:age屬性必須是數字
// 直接操作目標對象,不會觸發攔截
console.log(target.name); // 輸出:前端面試(無攔截日志)
target.age = '4'; // 無攔截日志,且不會觸發age的類型校驗
擴展延伸
Proxy API 的核心三要素是:“目標對象(target)”“攔截器對象(handler)”“代理實例(proxy)”。
- 目標對象(target):被代理的原始對象(可以是對象、數組、函數,甚至另一個 Proxy 實例);
- 攔截器(handler):包含 “攔截方法” 的對象,每個攔截方法對應一種目標對象的基礎操作(如get攔截屬性讀取,set攔截屬性賦值);
- 代理實例(proxy):通過new Proxy(target, handler) 創建的代理對象,所有對目標對象的操作需通過代理實例完成,才能觸發攔截器。也就是說,直接操作目標對象,就不會走代理。
Proxy 是一種非侵入性的 API,他不會修改目標對象本身的結構或方法,所有攔截邏輯都封裝在攔截器中,實現 “代理行為” 與 “目標對象” 的解耦,相對 Object.defineProperty 更加靈活。
攔截器方法除了基礎的 get/set 方法,需要注意一些特殊的攔截方法(has/deleteProperty/apply/construct):
| 攔截方法 | 作用 | 關鍵參數 | 適用場景 |
|---|---|---|---|
| get | 攔截屬性讀取(含 obj.prop、obj [prop]) | target, prop, receiver | 數據劫持(如響應式)、默認值設置 |
| set | 攔截屬性賦值 | target, prop, value, receiver | 數據校驗、值格式化 |
| has | 攔截 in 運算符(如prop in proxy) | target, prop | 權限控制(隱藏某些屬性不被檢測) |
| deleteProperty | 攔截 delete 操作(如delete proxy.prop) | target, prop | 禁止刪除關鍵屬性 |
| apply | 攔截函數調用(僅當 target 是函數時) | target, thisArg, args | 函數參數校驗、調用日志記錄 |
| construct | 攔截 new 操作(僅當 target 是構造函數時) | target, args, newTarget | 構造函數參數校驗、實例計數 |
如果你使用 Vue3 框架,需要知道的時 Vue3 的響應式設計就是基于 Proxy 的,這是一個簡化版的響應式 API 設計:
// 存儲當前活躍的副作用函數(如組件渲染函數)
let activeEffect = null;
// 依賴映射表:target → { prop → [effect1, effect2,...] }
const targetMap = new WeakMap();
// 1. 依賴收集函數:將副作用函數與target、prop關聯
function track(target, prop) {
if (!activeEffect) return; // 無活躍副作用,不收集
// 確保target在targetMap中存在映射
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 確保prop在depsMap中存在副作用數組
let deps = depsMap.get(prop);
if (!deps) {
deps = new Set(); // 用Set避免重復副作用
depsMap.set(prop, deps);
}
// 添加當前副作用函數
deps.add(activeEffect);
}
// 2. 依賴觸發函數:執行target、prop對應的所有副作用
function trigger(target, prop) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(prop);
if (deps) {
deps.forEach(effect => effect()); // 執行所有副作用
}
}
// 3. 響應式函數:創建Proxy代理,實現依賴收集與觸發
function reactive(target) {
return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
track(target, prop); // 讀取時收集依賴
// 若value是對象,遞歸創建響應式(深度響應)
if (typeof value === 'object' && value !== null) {
return reactive(value);
}
return value;
},
set(target, prop, value, receiver) {
const oldValue = Reflect.get(target, prop, receiver);
const success = Reflect.set(target, prop, value, receiver);
if (success && oldValue !== value) {
trigger(target, prop); // 賦值時觸發依賴
}
return success;
}
});
}
// 4. 副作用函數注冊:執行fn并收集其依賴
function effect(fn) {
activeEffect = fn;
fn(); // 執行fn,觸發get攔截,收集依賴
activeEffect = null; // 重置,避免后續誤收集
}
// 測試響應式
const data = reactive({ count: 0 });
// 注冊副作用函數(模擬組件渲染)
effect(() => {
console.log(`視圖更新:count = ${data.count}`);
});
// 修改數據,觸發副作用(視圖更新)
data.count = 1; // 輸出:視圖更新:count = 1
data.count = 2; // 輸出:視圖更新:count = 2
面試追問
- Proxy 和 Object.defineProperty 都能實現數據劫持,為什么 Vue3 放棄 Object.defineProperty 改用 Proxy?兩者的核心差異是什么?
| 對比維度 | Object.defineProperty | Proxy |
|---|---|---|
| 劫持范圍 | 僅能劫持 “對象的單個屬性”(需遍歷屬性逐個定義) | 直接劫持 “整個對象”(無需遍歷屬性) |
| 數組支持 | 無法劫持數組的原生方法(如 push、splice),需重寫數組原型 | 能攔截數組的所有操作(包括索引賦值、原生方法調用) |
| 嵌套對象處理 | 需遞歸遍歷所有嵌套對象,手動為每個屬性定義劫持 | 可在 get 攔截中遞歸創建代理(按需劫持,性能更優) |
| 性能 | 初始化時需遍歷所有屬性,嵌套層級深時性能差 | 懶加載式劫持(訪問嵌套對象時才創建代理),初始化性能更優 |
-
用 Proxy 攔截對象屬性賦值時,若目標對象是凍結對象(Object.freeze),set 攔截器還能生效嗎?為什么?
set 攔截器會觸發,但最終賦值會失敗,Object.freeze (target) 會讓目標對象的屬性變為 “不可寫、不可配置”,但不會阻止 Proxy 攔截器的觸發(攔截器是對操作的劫持,而非直接修改屬性)。 -
若用 Proxy 代理一個頻繁修改的大型對象(如包含 1000 個屬性的列表),會有性能問題嗎?如何優化?
會有性能問題,需要從兩方面考慮:1. 若攔截器邏輯復雜(如每次 get/set 都執行大量校驗、日志記錄),頻繁操作時會累積性能損耗;2. 對嵌套層級極深的大型對象,若初始化時遞歸創建代理(而非按需劫持),會導致初始化耗時過長。
優化方案:1. 簡化攔截器邏輯:將非必要操作(如詳細日志)改為條件觸發(如開發環境才執行),避免每次攔截都執行冗余代碼;2. 按需劫持(懶加載):僅在訪問嵌套對象時,才為其創建 Proxy(如 Vue3 的響應式實現),避免初始化時遞歸遍歷所有屬性;3. 跳過無意義攔截:對不需要劫持的屬性(如只讀屬性、常量),在攔截器中直接返回原始值,不執行額外邏輯;4. 使用 WeakMap 緩存代理實例:避免對同一目標對象重復創建 Proxy,減少內存占用與初始化開銷。


浙公網安備 33010602011771號