什么是Proxy代理?
Proxy 用于修改某些操作的默認行為,等同于在語言層面做出修改,所以屬于一種“元編程”(meta programming),即對編程語言進行編程。
Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀?。?code>get)和設置(set
)行為。這里暫時先不解釋具體的語法,只看運行結果。對設置了攔截行為的對象obj
,去讀寫它的屬性,就會得到下面的結果。
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代碼說明,Proxy 實際上重載(overload)了點運算符,即用自己的定義覆蓋了語言的原始定義。
如何定義Proxy代理?
ES6 原生提供 Proxy 構造函數(shù),用來生成 Proxy 實例。
var proxy = new Proxy(target, handler);
Proxy 對象的所有用法,都是上面這種形式,不同的只是handler參數(shù)的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數(shù)表示所要攔截的目標對象,handler參數(shù)也是一個對象,用來定制攔截行為。
下面是另一個攔截讀取屬性行為的例子。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
上面代碼中,作為構造函數(shù),Proxy接受兩個參數(shù)。第一個參數(shù)是所要代理的目標對象(上例是一個空對象),即如果沒有Proxy的介入,操作原來要訪問的就是這個對象;第二個參數(shù)是一個配置對象,對于每一個被代理的操作,需要提供一個對應的處理函數(shù),該函數(shù)將攔截對應的操作。比如,上面代碼中,配置對象有一個get方法,用來攔截對目標對象屬性的訪問請求。get方法的兩個參數(shù)分別是目標對象和所要訪問的屬性。可以看到,由于攔截函數(shù)總是返回35,所以訪問任何屬性都得到35。
注意,要使得Proxy起作用,必須針對Proxy實例(上例是proxy對象)進行操作,而不是針對目標對象(上例是空對象)進行操作。
如果handler沒有設置任何攔截,那就等同于直接通向原對象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代碼中,handler是一個空對象,沒有任何攔截效果,訪問proxy就等同于訪問target。
一個技巧是將 Proxy 對象,設置到object.proxy屬性,從而可以在object對象上調(diào)用。
var object = { proxy: new Proxy(target, handler) };
Proxy 實例也可以作為其他對象的原型對象。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代碼中,proxy對象是obj對象的原型,obj對象本身并沒有time屬性,所以根據(jù)原型鏈,會在proxy對象上讀取該屬性,導致被攔截。
同一個攔截器函數(shù),可以設置攔截多個操作。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
對于可以設置、但沒有設置攔截的操作,則直接落在目標對象上,按照原先的方式產(chǎn)生結果。
下面是 Proxy 支持的攔截操作一覽,一共 13 種。
- get(target, propKey, receiver):攔截對象屬性的讀取,比如proxy.foo和proxy['foo']。
- set(target, propKey, value, receiver):攔截對象屬性的設置,比如proxy.foo = v或proxy['foo'] = v,返回一個布爾值。
- has(target, propKey):攔截propKey in proxy的操作,返回一個布爾值。
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布爾值。
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環(huán),返回一個數(shù)組。該方法返回目標對象所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
- preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
- isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。如果目標對象是函數(shù),那么還有兩種額外操作可以攔截。
- apply(target, object, args):攔截 Proxy 實例作為函數(shù)調(diào)用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args):攔截 Proxy 實例作為構造函數(shù)調(diào)用的操作,比如new proxy(...args)。
如果你想了解更多ES6 Proxy內(nèi)容和詳細使用方法,這里有最通俗易懂的教程《ES6 Proxy》。