什么是Proxy代理
ES6 讓開發(fā)者能進一步接近 JS 引擎的能力,這些能力原先只存在于內(nèi)置對象上。語言通過代
理( proxy )暴露了在對象上的內(nèi)部工作,代理是一種封裝,能夠攔截并改變 JS 引擎的底
層操作。
人話是:把代理看做是設(shè)計模式代理模式中的一種,有一個代理對象來代理本體,而ES6的Proxy牛逼的一點是可以把本體沒法改變的內(nèi)部屬性改了
代理與反射是什么?
通過調(diào)用 new Proxy() ,你可以創(chuàng)建一個代理用來替代另一個對象(被稱為目標(biāo)),這個代理對目標(biāo)對象進行了虛擬,因此該代理與該目標(biāo)對象表面上可以被當(dāng)作同一個對象來對待。
代理允許你攔截在目標(biāo)對象上的底層操作,而這原本是JS引擎的內(nèi)部能力。攔截行為使用了一個能夠響應(yīng)特定操作的函數(shù)(被稱為陷阱)。
被 Reflect 對象所代表的反射接口,是給底層操作提供默認行為的方法的集合,這些操作是能夠被代理重寫的。每個代理陷阱都有一個對應(yīng)的反射方法,每個方法都與對應(yīng)的陷阱函數(shù)同名,并且接收的參數(shù)也與之一致。下表總結(jié)了這些行為:
| 代理陷阱 | 被重寫的行為 | 默認行為 |
|---|---|---|
| get | 讀取一個屬性的值 | Reflect.get() |
| set | 寫入一個屬性 | Reflect.set() |
| has | in 運算符 | Reflect.has() |
| deleteProperty | delete 運算符 | Reflect.deleteProperty() |
| getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
| setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
| isExtensible | Object.isExtensiable() | Reflect.setPrototypeOf() |
| preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
| defineProperty | Object.defineProperty() | Reflect.defineProperty() |
| ownKeys | Object.keys 、Object.getOwnPropertyNames() 與Object.getOwnPropertySymbols() | Reflect.ownKeys() |
| apply | 調(diào)用一個函數(shù) | Reflect.apply() |
| construct | 使用new調(diào)用一個函數(shù) | Reflect.construct() |
每個陷阱函數(shù)都可以重寫 JS 對象的一個特定內(nèi)置行為,允許你攔截并修改它。如果你仍然需
要使用原先的內(nèi)置行為,則可使用對應(yīng)的反射接口方法。一旦創(chuàng)建了代理,你就能清晰了解
代理與反射接口之間的關(guān)系,因此我們最好通過一些例子來進行深入研究。
創(chuàng)建一個簡單的代理
當(dāng)你使用 Proxy 構(gòu)造器來創(chuàng)建一個代理時,需要傳遞兩個參數(shù):目標(biāo)對象以及一個處理器(handler),后者是定義了一個或多個陷阱函數(shù)的對象。如果未提供陷阱函數(shù),代理會對所有操作采取默認行為。為了創(chuàng)建一個僅進行傳遞的代理,你需要使用不包含任何陷阱函數(shù)的處理器:
let target={}
let proxy=new Proxy(target,{})
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
使用 set 陷阱函數(shù)驗證屬性值
假設(shè)你想要創(chuàng)建一個對象,并要求其屬性值只能是數(shù)值,這就意味著該對象的每個新增屬性都要被驗證,并且在屬性值不為數(shù)值類型時應(yīng)當(dāng)拋出錯誤。為此你需要定義 set 陷阱函數(shù)來重寫設(shè)置屬性值時的默認行為,該陷阱函數(shù)能接受四個參數(shù):
- trapTarget :將接收屬性的對象(即代理的目標(biāo)對象);
- key :需要寫入的屬性的鍵(字符串類型或符號類型);
- value :將被寫入屬性的值;
- receiver :操作發(fā)生的對象(通常是代理對象)。
Reflect.set() 是 set 陷阱函數(shù)對應(yīng)的反射方法,同時也是set操作的默認行為。Reflect.set()方法與set陷阱函數(shù)一樣,能接受這四個參數(shù),讓該方法能在陷阱函數(shù)內(nèi)部被方便使用。該陷阱函數(shù)需要在屬性被設(shè)置完成的情況下返回 true ,否則就要返回 false,而 Reflect.set() 也會基于操作是否成功而返回相應(yīng)的結(jié)果。
你需要使用 set 陷阱函數(shù)來攔截傳入的 value 值,以便對屬性值進行驗證。這里有個例子:
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 忽略已有屬性,避免影響它們
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 添加屬性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加一個新屬性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 你可以為 name 賦一個非數(shù)值類型的值,因為該屬性已經(jīng)存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 拋出錯誤
proxy.anotherName = "proxy";
使用 get 陷阱函數(shù)進行對象外形驗證
該陷阱函數(shù)會在讀取屬性時被調(diào)用,即使該屬性在對象中并不存在,它能接受三個參數(shù):
- trapTarget :將會被讀取屬性的對象(即代理的目標(biāo)對象);
- key :需要讀取的屬性的鍵(字符串類型或符號類型);
- receiver :操作發(fā)生的對象(通常是代理對象)。
你可以使用 get 陷阱函數(shù)與 Reflect.get() 方法在目標(biāo)屬性不存在時拋出錯誤,就像這樣:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// 添加屬性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 讀取不存在屬性會拋出錯誤
console.log(proxy.nme); // 拋出錯誤
使用 has 陷阱函數(shù)隱藏屬性
has 陷阱函數(shù)會在使用 in 運算符的情況下被調(diào)用,并且會被傳入兩個參數(shù):
- trapTarget :需要讀取屬性的對象(即代理的目標(biāo)對象);
- key :需要檢查的屬性的鍵(字符串類型或符號類型)。
Reflect.has() 方法接受與之相同的參數(shù),并向 in 運算符返回默認響應(yīng)結(jié)果。使用 has陷阱函數(shù)以及 Reflect.has() 方法,允許你修改部分屬性在接受 in 檢測時的行為,但保留其他屬性的默認行為。例如,假設(shè)你只想要隱藏 value 屬性,你可以這么做:
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true
這里的 proxy 對象使用了 has 陷阱函數(shù),用于檢查 key 值是否為 "value" 。如果是,則返回 false ;否則通過調(diào)用 Reflect.has() 方法來返回默認的結(jié)果。這樣,雖然 value 屬性確實存在于目標(biāo)對象中,但 in 運算符卻會對該屬性返回 false ;而其他的屬性(name 與 toString )則會正確地返回 true 。
使用 deleteProperty 陷阱函數(shù)避免屬性被刪除
deleteProperty 陷阱函數(shù)會在使用 delete 運算符去刪除對象屬性時下被調(diào)用,并且會被傳入兩個參數(shù):
- trapTarget :需要刪除屬性的對象(即代理的目標(biāo)對象);
- key :需要刪除的屬性的鍵(字符串類型或符號類型)。
Reflect.deleteProperty() 方法也接受這兩個參數(shù),并提供了 deleteProperty 陷阱函數(shù)的默認實現(xiàn)。你可以結(jié)合 Reflect.deleteProperty() 方法以及 deleteProperty 陷阱函數(shù),來修改 delete 運算符的行為。例如,能確保 value 屬性不被刪除:
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
// 嘗試刪除 proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// 嘗試刪除 proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
原型代理的陷阱函數(shù)
ES6 引入該方法用于對 ES5 的Object.getPrototypeOf() 方法進行補充。代理允許你通過 setPrototypeOf 與
getPrototypeOf 陷阱函數(shù)來對這兩個方法的操作進行攔截。Object對象上的這兩個方法都會調(diào)用代理中對應(yīng)名稱的陷阱函數(shù),從而允許你改變這兩個方法的行為。
setPrototypeOf 陷阱函數(shù)接受三個參數(shù):
- trapTarget :需要設(shè)置原型的對象(即代理的目標(biāo)對象);
- proto :需用被用作原型的對象。
getPrototypeOf 陷阱函數(shù)的返回值必須是一個對象或者null,其他任何類型的返回值都會引發(fā)“運行時”錯誤。對于返回值的檢測確保了Object.getPrototypeOf() 會返回預(yù)期的結(jié)果。類似的, setPrototypeOf 必須在操作沒有成功的情況下返回 false ,這樣會讓 Object.setPrototypeOf()拋出錯誤;而若setPrototypeOf的返回值不是false,則Object.setPrototypeOf() 就會認為操作已成功。
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 拋出錯誤
Object.setPrototypeOf(proxy, {});
如果你想在這兩個陷阱函數(shù)中使用默認的行為,那么只需調(diào)用Reflect對象上的相應(yīng)方法。例如,下面的代碼為getPrototypeOf 方法與 setPrototypeOf 方法實現(xiàn)了默認的行為:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// 成功
Object.setPrototypeOf(target, {});
// 同樣成功
Object.setPrototypeOf(proxy, {});
對象可擴展性的陷阱函數(shù)
ES5 通過 Object.preventExtensions() 與 Object.isExtensible() 方法給對象增加了可擴展性。而 ES6 則通過 preventExtensions 與 isExtensible 陷阱函數(shù)允許代理攔截對于底層對象的方法調(diào)用。
isExtensible 陷阱函數(shù)必須返回一個布爾值用于表明目標(biāo)對象是否可被擴展,而preventExtensions陷阱函數(shù)也需要返回一個布爾值,用于表明操作是否已成功。同時也存在 Reflect.preventExtensions() 與 Reflect.isExtensible() 方法,用于實現(xiàn)默認的行為。這兩個方法都返回布爾值,因此它們可以在對應(yīng)的陷阱函數(shù)內(nèi)直接使用。
為了弄懂對象可擴展性的陷阱函數(shù)如何運作,可研究如下代碼,該代碼實現(xiàn)了 isExtensible與 preventExtensions 陷阱函數(shù)的默認行為。
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
屬性描述符的陷阱函數(shù)
ES5 最重要的特征之一就是引入了Object.defineProperty()方法用于定義屬性的特性。在JS之前的版本中,沒有方法可以定義一個訪問器屬性,也不能讓屬性變成只讀或是不可枚舉。而這些特性都能夠利用Object.defineProperty()方法來實現(xiàn),并且你還可以利用Object.getOwnPropertyDescriptor() 方法來檢索這些特性。
代理允許你使用 defineProperty 與 getOwnPropertyDescriptor 陷阱函數(shù),來分別攔截對Object.defineProperty() 與 Object.getOwnPropertyDescriptor() 的調(diào)用。 defineProperty陷阱函數(shù)接受下列三個參數(shù):
- trapTarget :需要被定義屬性的對象(即代理的目標(biāo)對象);
- key :屬性的鍵(字符串類型或符號類型);
- descriptor :為該屬性準(zhǔn)備的描述符對象。
默認的行為:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"
阻止 Object.defineProperty(
defineProperty 陷阱函數(shù)要求你返回一個布爾值用于表示操作是否已成功。當(dāng)它返回 true時, Object.defineProperty() 會正常執(zhí)行;而如果它返回了 false ,則Object.defineProperty() 會拋出錯誤。
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 拋出錯誤
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
描述符對象的限制
為了確保 Object.defineProperty() 與 Object.getOwnPropertyDescriptor() 方法的行為一致,傳遞給 defineProperty 陷阱函數(shù)的描述符對象必須是正規(guī)的。出于同一原因,getOwnPropertyDescriptor陷阱函數(shù)返回的對象也始終需要被驗證。
任意對象都能作為 Object.defineProperty() 方法的第三個參數(shù);然而傳遞給defineProperty 陷阱函數(shù)的描述符對象參數(shù),則只有 enumerable 、 configurable 、value 、 writable 、 get 與 set 這些屬性是被許可的。
ownKeys 陷阱函數(shù)
ownKeys 代理陷阱攔截了內(nèi)部方法[[OwnPropertyKeys]],并允許你返回一個數(shù)組用于重寫該行為。返回的這個數(shù)組會被用于四個方法: Object.keys() 方法、Object.getOwnPropertyNames() 方法、 Object.getOwnPropertySymbols() 方法與
Object.assign() 方法,其中 Object.assign() 方法會使用該數(shù)組來決定哪些屬性會被復(fù)制。
ownKeys 陷阱函數(shù)的默認行為由Reflect.ownKeys()方法實現(xiàn),會返回一個由全部自有屬性的鍵構(gòu)成的數(shù)組,無論鍵的類型是字符串還是符號。 Object.getOwnProperyNames() 方法與Object.keys() 方法會將符號值從該數(shù)組中過濾出去;相反,
Object.getOwnPropertySymbols() 會將字符串值過濾掉;而Object.assign()方法會使用數(shù)組中所有的字符串值與符號值。
ownKeys 陷阱函數(shù)接受單個參數(shù),即目標(biāo)對象,同時必須返回一個數(shù)組或者一個類數(shù)組對象,不合要求的返回值會導(dǎo)致錯誤。你可以使用 ownKeys 陷阱函數(shù)去過濾特定的屬性,以避免這些屬性被Object.keys()方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols()方法或Object.assign()方法使用。假設(shè)你不想在結(jié)果中包含任何以下劃線打頭的屬性(在 JS 的編碼慣例中,這代表該字段是私有的),那么可以使用ownKeys陷阱函數(shù)來將它們過濾掉,就像下面這樣:
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy);
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
ownKeys 陷阱函數(shù)也能影響 for-in 循環(huán),因為這種循環(huán)調(diào)用了陷阱函數(shù)來決定哪些值能夠被用在循環(huán)內(nèi)。
使用 apply 與 construct 陷阱函數(shù)的函數(shù)代理
在所有的代理陷阱中,只有 apply 與 construct 要求代理目標(biāo)對象必須是一個函數(shù)。函數(shù)擁有兩個內(nèi)部方法: [[Call]] 與 [[Construct]] ,前者會在函數(shù)被直接調(diào)用時執(zhí)行,而后者會在函數(shù)被使用 new 運算符調(diào)用時執(zhí)行。 apply 與 construct陷阱函數(shù)對應(yīng)著這兩個內(nèi)部方法,并允許你對其進行重寫。當(dāng)不使用 new 去調(diào)用一個函數(shù)時, apply 陷阱函數(shù)會接收到下列三個參數(shù)( Reflect.apply() 也會接收這些參數(shù)):
- trapTarget :被執(zhí)行的函數(shù)(即代理的目標(biāo)對象);
- thisArg :調(diào)用過程中函數(shù)內(nèi)部的 this 值;
- argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。
當(dāng)使用 new 去執(zhí)行函數(shù)時, construct 陷阱函數(shù)會被調(diào)用并接收到下列兩個參數(shù):
- trapTarget :被執(zhí)行的函數(shù)(即代理的目標(biāo)對象);
- argumentsList :被傳遞給函數(shù)的參數(shù)數(shù)組。
因此,可以用來做很多騷操作,比如
調(diào)用構(gòu)造器而無須使用 new
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentsList) {
return Reflect.construct(trapTarget, argumentsList);
}
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
可被撤銷的代理
在被創(chuàng)建之后,代理通常就不能再從目標(biāo)對象上被解綁。本章之前的例子都使用了不可被撤銷的代理,但有的情況下你可能想撤銷一個代理以便讓它不能再被使用。當(dāng)你想通過公共接口向外提供一個安全的對象,并且要求要隨時都能切斷對某些功能的訪問,這種情況下可被撤銷的代理就會非常有用。
你可以使用 Proxy.revocable()方法來創(chuàng)建一個可被撤銷的代理,該方法接受的參數(shù)與Proxy構(gòu)造器的相同:一個目標(biāo)對象- 、一個代理處理器,而返回值是包含下列屬性的一個對象:
- proxy :可被撤銷的代理對象;
- revoke :用于撤銷代理的函數(shù)。
當(dāng) revoke() 函數(shù)被調(diào)用后,就不能再對該proxy對象進行更多操作,任何與該代理對象交互的意圖都會觸發(fā)代理的陷阱函數(shù),從而拋出一個錯誤。
總結(jié)
在 ES6 之前,特定對象(例如數(shù)組)會顯示出一些非常規(guī)的、無法被開發(fā)者復(fù)制的行為,而代理的出現(xiàn)改變了這種情況。代理允許你為一些 JS 底層操作自行定義非常規(guī)行為,因此你就可以通過代理陷阱來復(fù)制JS內(nèi)置對象的所有行為。在各種不同操作發(fā)生時(例如對于 in運算符的使用),這些代理陷阱會在后臺被調(diào)用。
反射接口也是在 ES6 中引入的,允許開發(fā)者為每個代理陷阱實現(xiàn)默認的行為。每個代理陷阱在 Reflect 對象( ES6 的另一個新特性)上都有一個同名的對應(yīng)方法。將代理陷阱與反射接口方法結(jié)合使用,就可以在特定條件下讓一些操作有不同的表現(xiàn),有別于默認的內(nèi)置行為。
可被撤銷的代理是一種特殊的代理,可以使用revoke()函數(shù)去有效禁用。revoke()函數(shù)終結(jié)了代理的所有功能,因此在它被調(diào)用之后,所有與代理屬性交互的意圖都會導(dǎo)致拋出錯誤。第三方開發(fā)者可能需要在一定時間內(nèi)獲取特定對象的使用權(quán),在這種場合,可被撤銷的代理對應(yīng)用的安全性來說就非常重要。
盡管直接使用代理是最有力的使用方式,但你也可以把代理用作另一個對象的原型。但只有很少的代理陷阱能在作為原型的代理上被有效使用,包括 get 、 set 與 has 這幾個,這讓這方面的用例變得十分有限。