1.概述
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);
}
});
上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀取(get)和設置(set)行爲。這裏暫時先不解釋具體的語法,只看運行結果。對設置了攔截行爲的對象obj,去讀寫它的屬性,就會得到下面的結果。
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代碼說明,Proxy 實際上重載(overload)了點運算符,即用自己的定義覆蓋了語言的原始定義。
ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例。
var proxy = new Proxy(target, handler);
Proxy 對象的所有用法,都是上面這種形式,不同的只是handler參數的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定製攔截行爲。
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循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而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),返回一個布爾值。如果目標對象是函數,那麼還有兩種額外操作可以攔截。
apply(target, object, args):攔截 Proxy 實例作爲函數調用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):攔截 Proxy 實例作爲構造函數調用的操作,比如new proxy(...args)。
2.Proxy 實例的方法
get()
get方法用於攔截某個屬性的讀取操作,可以接受三個參數,依次爲目標對象、屬性名和 proxy 實例本身(嚴格地說,是操作行爲所針對的對象),其中最後一個參數可選。
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 拋出一個錯誤
set()
set方法用來攔截某個屬性的賦值操作,可以接受四個參數,依次爲目標對象、屬性名、屬性值和 Proxy 實例本身,其中最後一個參數可選。
假定Person對象有一個age屬性,該屬性應該是一個不大於 200 的整數,那麼可以使用Proxy保證age的屬性值符合要求。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 對於滿足條件的 age 屬性以及其他屬性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
apply()
apply方法攔截函數的調用、call和apply操作。
apply方法可以接受三個參數,分別是目標對象、目標對象的上下文對象(this)和目標對象的參數數組。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
剩下的方法可參考https://es6.ruanyifeng.com/?search=proxy&x=0&y=0#docs/proxy
3.Proxy.revocable()
Proxy.revocable()方法返回一個可取消的 Proxy 實例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
4.this問題
雖然 Proxy 可以代理針對目標對象的訪問,但它不是目標對象的透明代理,即不做任何攔截的情況下,也無法保證與目標對象的行爲一致。主要原因就是在 Proxy 代理的情況下,目標對象內部的this關鍵字會指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
一旦proxy代理target.m,後者內部的this就是指向proxy,而不是target。