JS帶你深入領(lǐng)略Proxy的世界
1. Proxy 的基本結(jié)構(gòu)
Proxy 的基本使用方式:
/** * target: 表示要代理的目標,可以是object, array, function類型 * handler: 是一個對象,可以編寫各種代理的方法 */ const proxy = new Proxy(target, handler);
例如我們想要代理一個對象,可以通過設置 get 和 set 方法來代理獲取和設置數(shù)據(jù)的操作:
const person = {
name: 'wenzi',
age: 20,
};
const personProxy = new Proxy(person, {
get(target, key, receiver) {
console.log(`get value by ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`set ${key}, old value ${target[key]} to ${value}`);
target[key] = value;
},
});
Proxy 僅僅是一個代理,personProxy 上有 person 所有的屬性和方法。我們通過personProxy獲取和設置 name 時,就會有相應的 log 輸出:
personProxy.name; // "wenzi" // log: get value by name personProxy.name = 'hello'; // log: set name, old value wenzi to hello
并且通過 personProxy 設置數(shù)據(jù)時,代理的原結(jié)構(gòu)里的數(shù)據(jù)也會發(fā)生變化。我們打印下 person,可以發(fā)現(xiàn)字段 name 的值 也變成了hello:
console.log(person); // {name: "hello", age: 20}
Proxy 的第 2 個參數(shù) handler 除了可以設置 get 和 set 方法外,還有更多豐富的方法:
1.get(target, propKey, receiver):攔截對象屬性的讀取,比如 proxy.foo 和 proxy['foo']。
2.set(target, propKey, value, receiver):攔截對象屬性的設置,比如 proxy.foo = v 或 proxy['foo'] = v,返回一個布爾值。
3.has(target, propKey):攔截 propKey in proxy 的操作,返回一個布爾值。
4.deleteProperty(target, propKey):攔截 delete proxy[propKey]的操作,返回一個布爾值。
5.ownKeys(target):攔截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循環(huán),返回一個數(shù)組。該方法返回目標對象所有自身的屬性的屬性名,而 Object.keys()的返回結(jié)果僅包括目標對象自身的可遍歷屬性。
6.getOwnPropertyDescriptor(target, propKey):攔截 Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
7.defineProperty(target, propKey, propDesc):攔截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
8.preventExtensions(target):攔截 Object.preventExtensions(proxy),返回一個布爾值。
9.getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy),返回一個對象。
10.isExtensible(target):攔截 Object.isExtensible(proxy),返回一個布爾值。
11.setPrototypeOf(target, proto):攔截 Object.setPrototypeOf(proxy, proto),返回一個布爾值。如果目標對象是函數(shù),那么還有兩種額外操作可以攔截。
12.apply(target, object, args):攔截 Proxy 實例作為函數(shù)調(diào)用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
13.construct(target, args):攔截 Proxy 實例作為構(gòu)造函數(shù)調(diào)用的操作,比如 new proxy(...args)。
如我們通過 delete 刪除其中一個元素時,可以通過deleteProperty()方法來攔截這個操作。還是上面代理 person 的代碼,我們添加一個 deleteProperty:
const person = {
name: 'wenzi',
age: 20,
};
const personProxy = new Proxy(person, {
// 忽略get和set方法,與上面一樣
// ...
deleteProperty(target, key, receiver) {
console.log(`delete key ${key}`);
delete target[key];
},
});
當執(zhí)行 delete 操作時:
delete personProxy['age']; // log: delete key age
2. Proxy 與 Reflect
Proxy 與 Reflect 可以說形影不離了,Reflect 里所有的方法和使用方式與 Proxy 完全一樣。
例如上面 Proxy 里的 get(), set()和 deleteProperty()方法我們都是直接操作原代理對象的,這里我們改成使用Reflect來操作:
const personProxy = new Proxy(person, {
get(target, key, receiver) {
console.log(`get value by ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`set ${key}, old value ${target[key]} to ${value}`);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key, receiver) {
console.log(`delete key ${key}`);
return Reflect.deleteProperty(target, key, receiver);
},
});
可以發(fā)現(xiàn)完美地實現(xiàn)這些功能。

3. 代理數(shù)組
我們在之前的文章 Vue 中對數(shù)組特殊的操作 中,討論過 Vue 為什么沒有使用Object.defineProperty來劫持數(shù)據(jù),而是重寫了 Array 原型鏈上的幾個方法,通過這幾個方法來實現(xiàn) Vue 模板中數(shù)據(jù)的更新。
但若 Proxy 的話,就可以直接代理數(shù)組:
const arr = [1, 2, 3, 4];
const arrProxy = new Proxy(arr, {
get(target, key, receiver) {
console.log('arrProxy.get', target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('arrProxy.set', target, key, value);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log('arrProxy.deleteProperty', target, key);
return Reflect.deleteProperty(target, key);
},
});
現(xiàn)在我們再來操作一下代理后的數(shù)組 arrProxy 看下:
arrProxy[2] = 22; // arrProxy.set (4) [1, 2, 3, 4] 2 22 arrProxy[3]; // arrProxy.get (4) [1, 2, 22, 4] 3 delete arrProxy[2]; // arrProxy.deleteProperty (4) [1, 2, 22, 4] 2 arrProxy.push(5); // push操作比較復雜,這里進行了多個get()和set()操作 arrProxy.length; // arrProxy.get (5) [1, 2, empty, 4, 5] length
可以看到無論獲取、刪除還是修改數(shù)據(jù),都可以感知到。還有數(shù)組原型鏈上的一些方法,如:
1.push()
2.pop()
3.shift()
4.unshift()
5.splice()
6.sort()
7.reverse()
也都能通過 Proxy 中的代理方法劫持到。
concat()方法比較特殊的是,他是一個賦值操作,并不改變原數(shù)組,因此在調(diào)用 concat()方法操作數(shù)組時,如果沒有賦值操作,那么這里只有 get()攔截到。

4. 代理函數(shù)
Proxy 中還有一個apply()方法,是表示自己作為函數(shù)調(diào)用時,被攔截的操作。
const getSum = (...args) => {
if (!args.every((item) => typeof item === 'number')) {
throw new TypeError('參數(shù)應當均為number類型');
}
return args.reduce((sum, item) => sum + item, 0);
};
const fnProxy = new Proxy(getSum, {
/**
* @params {Fuction} target 代理的對象
* @params {any} ctx 執(zhí)行的上下文
* @params {any} args 參數(shù)
*/
apply(target, ctx, args) {
console.log('ctx', ctx);
console.log(`execute fn ${getSum.name}, args: ${args}`);
return Reflect.apply(target, ctx, args);
},
});
執(zhí)行 fnProxy:
// 10, ctx為undefined, log: execute fn getSum, args: 1,2,3,4 fnProxy(1, 2, 3, 4); // ctx為undefined, Uncaught TypeError: 參數(shù)應當均為number類型 fnProxy(1, 2, 3, '4'); // 10, ctx為window, log: execute fn getSum, args: 1,2,3,4 fnProxy.apply(window, [1, 2, 3, 4]); // 6, ctx為window, log: execute fn getSum, args: 1,2,3 fnProxy.call(window, 1, 2, 3); // 6, ctx為person, log: execute fn getSum, args: 1,2,3 fnProxy.apply(person, [1, 2, 3]);
5. 一些簡單的應用場景
我們知道 Vue3 里已經(jīng)用 Proxy 重寫了響應式系統(tǒng),mobx 也已經(jīng)用了 Proxy 模式。在可見的未來,會有更多的 Proxy 的應用場景,我們這里也稍微講解幾個。
5.1 統(tǒng)計函數(shù)被調(diào)用的上下文和次數(shù)
這里我們用 Proxy 來代理函數(shù),然后函數(shù)被調(diào)用的上下文和次數(shù)。
const countExecute = (fn) => {
let count = 0;
return new Proxy(fn, {
apply(target, ctx, args) {
++count;
console.log('ctx上下文:', ctx);
console.log(`${fn.name} 已被調(diào)用 ${count} 次`);
return Reflect.apply(target, ctx, args);
},
});
};
現(xiàn)在我們來代理下剛才的getSum()方法:
const getSum = (...args) => {
if (!args.every((item) => typeof item === 'number')) {
throw new TypeError('參數(shù)應當均為number類型');
}
return args.reduce((sum, item) => sum + item, 0);
};
const useSum = countExecute(getSum);
useSum(1, 2, 3); // getSum 已被調(diào)用 1 次
useSum.apply(window, [2, 3, 4]); // getSum 已被調(diào)用 2 次
useSum.call(person, 3, 4, 5); // getSum 已被調(diào)用 3 次
5.2 實現(xiàn)一個防抖功能
基于上面統(tǒng)計函數(shù)調(diào)用次數(shù)的功能,也給我們實現(xiàn)一個函數(shù)的防抖功能添加了靈感。
const throttleByProxy = (fn, rate) => {
let lastTime = 0;
return new Proxy(fn, {
apply(target, ctx, args) {
const now = Date.now();
if (now - lastTime > rate) {
lastTime = now;
return Reflect.apply(target, ctx, args);
}
},
});
};
const logTimeStamp = () => console.log(Date.now());
window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300));
logTimeStamp()至少需要 300ms 才能執(zhí)行一次。
5.3 實現(xiàn)觀察者模式
我們在這里實現(xiàn)一個最簡單類 mobx 觀察者模式。
const list = new Set();
const observe = (fn) => list.add(fn);
const observable = (obj) => {
return new Proxy(obj, {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
list.forEach((observer) => observer());
return result;
},
});
};
const person = observable({ name: 'wenzi', age: 20 });
const App = () => {
console.log(`App -> name: ${person.name}, age: ${person.age}`);
};
observe(App);
person就是使用 Proxy 創(chuàng)建出來的代理對象,每當 person 中的屬性發(fā)生變化時,就會執(zhí)行 App()函數(shù)。這樣就實現(xiàn)了一個簡單的響應式狀態(tài)管理。
6. Proxy 與 Object.defineProperty 的對比
上面很多例子用Object.defineProperty也都是可以實現(xiàn)的。那么這兩者都各有什么優(yōu)缺點呢?
6.1 Object.defineProperty 的優(yōu)劣
Object.defineProperty的兼容性可以說比 Proxy 要好很多,出特別低的 IE6,IE7 瀏覽器外,其他瀏覽器都有支持。
但 Object.defineProperty 支持的方法很多,并且主要是基于屬性進行攔截的。因此在 Vue2 中只能重寫 Array 原型鏈上的方法,來操作數(shù)組。
6.2 Proxy 的優(yōu)劣
Proxy與上面的正好相反,Proxy 是基于對象來進行代理的,因此可代理更多的類型,例如 Object, Array, Function 等;而且代理的方法也多了很多。
劣勢就是兼容性不太好,即使用 polyfill,也無法完美的實現(xiàn)。
7. 總結(jié)
Proxy 能實現(xiàn)的功能還有很多,后面我們也會繼續(xù)進行探索,并且盡可能去了解下基于 Proxy 實現(xiàn)的類庫,例如 mobx5 的源碼和實現(xiàn)原理等。
以上就是JS帶你深入領(lǐng)略Proxy的世界的詳細內(nèi)容,更多關(guān)于JS中的代理Proxy的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Javascript表單特效之十大常用原理性樣例代碼大總結(jié)
開頭說這個常用原理性樣例,大家可能不太清楚,這篇文章主要是針對實際開發(fā)中常用的一些代碼分析,主要是針對表單處理方法的一些資料,推薦大家收藏2016-07-07
如何正確使用javascript 來進行我們的程序開發(fā)
Javascript 正確使用方法,下面為大家介紹的是一個關(guān)于如何正確使用javascript 來進行我們的程序開發(fā),需要的朋友可以參考下2014-06-06
Javascript中匿名函數(shù)的多種調(diào)用方式總結(jié)
這篇文章主要是對Javascript中匿名函數(shù)的多種調(diào)用方式進行了詳細的總結(jié)介紹。需要的朋友可以過來參考下,希望對大家有所幫助2013-12-12
重寫JS setTimeout 方法 JavaScript Hook
想要重寫 setTimeout 方法,發(fā)現(xiàn)有動態(tài)引入的js,需要改成自己的js,以下教教大家這個需求,防止網(wǎng)站被劫持2023-07-07
bootstrap table 數(shù)據(jù)表格行內(nèi)修改的實現(xiàn)代碼
這篇文章主要介紹了bootstrap table 數(shù)據(jù)表格行內(nèi)修改的實現(xiàn)代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-02-02

