【每日一面】你怎么理解 Proxy 的(de)
基礎問答
問:Proxy 是什么(me)?怎么(me)使用的(de)?
答(da):Proxy 是用(yong)于創建 “對(dui)(dui)象(xiang)(xiang)(xiang)代理” 的構造函數,它(ta)能封裝目標對(dui)(dui)象(xiang)(xiang)(xiang)(target),并通過 “攔截器對(dui)(dui)象(xiang)(xiang)(xiang)(handler)” 自(zi)定義目標對(dui)(dui)象(xiang)(xiang)(xiang)的基礎(chu)操作(如屬性讀取、賦值),實現(xian)對(dui)(dui)對(dui)(dui)象(xiang)(xiang)(xiang)行為的 “劫(jie)持”,手寫使用(yong)方(fang)式。
// 語法: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 的核心三要(yao)素是:“目標對(dui)象(xiang)(target)”“攔截器對(dui)象(xiang)(handler)”“代理實例(proxy)”。
- 目標對象(target):被代理的原始對象(可以是對象、數組、函數,甚至另一個 Proxy 實例);
- 攔截器(handler):包含 “攔截方法” 的對象,每個攔截方法對應一種目標對象的基礎操作(如get攔截屬性讀取,set攔截屬性賦值);
- 代理實例(proxy):通過new Proxy(target, handler) 創建的代理對象,所有對目標對象的操作需通過代理實例完成,才能觸發攔截器。也就是說,直接操作目標對象,就不會走代理。
Proxy 是一種非侵入性的 API,他不會修改目標對象本身的結構或方法,所有攔截邏輯都封裝在攔截器中,實現 “代理行為” 與 “目標對象” 的解耦,相對 Object.defineProperty 更加靈活。
攔截器方(fang)法(fa)除(chu)了基礎的(de) get/set 方(fang)法(fa),需要(yao)注意一些(xie)特殊的(de)攔截方(fang)法(fa)(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 | 構造函數參數校驗、實例計數 |
如果你使(shi)用 Vue3 框(kuang)架,需要知道的(de)(de)(de)(de)時 Vue3 的(de)(de)(de)(de)響應(ying)式設(she)計就是(shi)基于 Proxy 的(de)(de)(de)(de),這是(shi)一個簡化版的(de)(de)(de)(de)響應(ying)式 API 設(she)計:
// 存儲當前活躍的副作用函數(如組件渲染函數)
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 攔截(jie)器(qi)(qi)會觸發(fa),但最終賦值會失敗,Object.freeze (target) 會讓目標對象的(de)(de)屬性變為 “不可寫、不可配置(zhi)”,但不會阻止(zhi) Proxy 攔截(jie)器(qi)(qi)的(de)(de)觸發(fa)(攔截(jie)器(qi)(qi)是對操作的(de)(de)劫持,而非直(zhi)接(jie)修改(gai)屬性)。 -
若用 Proxy 代理一個頻繁修改的大型對象(如包含 1000 個屬性的列表),會有性能問題嗎?如何優化?
會有性能問題,需要從兩方面考慮:1. 若攔截器邏輯復雜(如每次 get/set 都執行大量校驗、日志記錄),頻繁操作時會累積性能損耗;2. 對嵌套層級極深的大型對象,若初始化時遞歸創建代理(而非按需劫持),會導致初始化耗時過長。
優化(hua)(hua)(hua)方案(an):1. 簡化(hua)(hua)(hua)攔(lan)截器(qi)邏(luo)輯:將非必要操作(zuo)(如(ru)詳細(xi)日志)改為條件觸發(如(ru)開(kai)發環境才執行),避(bi)(bi)(bi)免每次攔(lan)截都執行冗余代碼;2. 按(an)需(xu)劫(jie)持(懶加載):僅在訪問嵌套(tao)對(dui)象時(shi)(shi),才為其創建 Proxy(如(ru) Vue3 的響應式實現(xian)),避(bi)(bi)(bi)免初始化(hua)(hua)(hua)時(shi)(shi)遞歸遍歷所有屬性;3. 跳過無(wu)意義(yi)攔(lan)截:對(dui)不需(xu)要劫(jie)持的屬性(如(ru)只(zhi)讀屬性、常(chang)量),在攔(lan)截器(qi)中直(zhi)接(jie)返回原(yuan)始值,不執行額(e)外邏(luo)輯;4. 使用(yong) WeakMap 緩存代理(li)實例:避(bi)(bi)(bi)免對(dui)同一目標對(dui)象重(zhong)復創建 Proxy,減(jian)少內存占用(yong)與初始化(hua)(hua)(hua)開(kai)銷。
本(ben)文首(shou)發于,公(gong)眾號訂閱(yue)請關(guan)注:

