深入了解 Proxy 代理
代理對象封裝另一個對象并攔截操作,如讀取/寫入屬性和其他操作,可以選擇自己處理它們,或透明地允許對象處理它們。
很多庫和一些瀏覽器框架都使用代理。在本文中,我們將看到許多實(shí)際應(yīng)用程序。
Proxy
語法如下:
let proxy = new Proxy(target, handler)
target - 是一個要包裝的對象,可以是任何東西,包括函數(shù)。
handler - 代理配置:一個帶有“陷阱”的對象,攔截操作的方法。-例如,讀取target屬性時設(shè)置trap,寫入target屬性時設(shè)置trap,等等。
對于代理上的操作,如果handler中有相應(yīng)的陷阱,那么它就會運(yùn)行,并且代理有機(jī)會處理它,否則操作就會在目標(biāo)上執(zhí)行。
作為一個開始的例子,讓我們創(chuàng)建一個沒有任何陷阱的代理:
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 5; // writing to proxy (1)
alert(target.test); // 5, the property appeared in target!
alert(proxy.test); // 5, we can read it from proxy too (2)
for(let key in proxy) alert(key); // test, iteration works (3)
由于沒有陷阱,代理上的所有操作都被轉(zhuǎn)發(fā)到目標(biāo)。
寫操作 proxy.test=target上的值。
讀取操作 proxy.test 從 target 返回值。
迭代代理返回目標(biāo)值。
正如我們所見,沒有任何陷阱,proxy是一個透明的目標(biāo)包裝器。
Proxy是一種特殊的“外來對象”。它沒有自己的屬性。使用空處理程序,它透明地將操作轉(zhuǎn)發(fā)給target。

為了激活更多的功能,讓我們添加陷阱。
我們能用他們攔截什么?
對于對象上的大多數(shù)操作,JavaScript規(guī)范中都有一個所謂的“內(nèi)部方法”,它描述了它在最低級別的工作方式。例如[[Get]],讀取屬性的內(nèi)部方法,[[Set]],寫入屬性的內(nèi)部方法,等等。這些方法僅在規(guī)范中使用,我們不能直接通過名稱調(diào)用它們。
代理陷阱攔截這些方法的調(diào)用。它們在代理規(guī)范和下表中列出。
對于每個內(nèi)部方法,在該表中都有一個陷阱:我們可以添加到新代理的handler參數(shù)的方法名來攔截操作:

使用 get 方式獲取默認(rèn)值
最常見的陷阱是用于讀/寫屬性的。
為了攔截讀取,處理程序應(yīng)該有一個方法get(目標(biāo)、屬性、接收器)。
當(dāng)一個屬性被讀取時,它會觸發(fā),參數(shù)如下:
target—是目標(biāo)對象,作為第一個參數(shù)傳遞給新代理,
property -屬性名稱,
receiver——如果目標(biāo)屬性是一個getter,那么receiver就是將在其調(diào)用中使用的對象。通常這是代理對象本身(或者從它繼承的對象,如果我們從代理繼承的話)。現(xiàn)在我們不需要這個論證,所以后面會更詳細(xì)地解釋。
讓我們使用get來實(shí)現(xiàn)對象的默認(rèn)值。
我們將創(chuàng)建一個數(shù)字?jǐn)?shù)組,對于不存在的值返回0。
通常,當(dāng)一個人試圖獲取一個不存在的數(shù)組項(xiàng)時,他們得到的是未定義的,但是我們將把一個常規(guī)的數(shù)組包裝到代理中,以捕獲讀取,如果沒有這樣的屬性則返回0:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // default value
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (no such item)
正如我們所見,誘捕陷阱很容易做到。
我們可以使用代理來實(shí)現(xiàn)“默認(rèn)”值的任何邏輯。
想象一下我們有一本詞典,里面有一些短語和它們的翻譯:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
現(xiàn)在,如果沒有短語,從字典中讀取將返回undefined。但在實(shí)踐中,不翻譯一個短語通常比不定義要好。我們讓它返回一個未翻譯的短語,而不是undefined。
為了實(shí)現(xiàn)這一點(diǎn),我們將把dictionary封裝在一個攔截讀取操作的代理中:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // intercept reading a property from dictionary
if (phrase in target) { // if we have it in the dictionary
return target[phrase]; // return the translation
} else {
// otherwise, return the non-translated phrase
return phrase;
}
}
});
// Look up arbitrary phrases in the dictionary!
// At worst, they're not translated.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (no translation)
使用 set 驗(yàn)證
假設(shè)我們想要一個專門用于數(shù)字的數(shù)組。如果添加了另一種類型的值,應(yīng)該會出現(xiàn)錯誤。
set trap在寫入屬性時觸發(fā)。
set(target, property, value, receiver)
target—是目標(biāo)對象,作為第一個參數(shù)傳遞給新代理,
property -屬性名稱,
value -屬性值,
receiver——與get trap類似,只對setter屬性有效。
如果設(shè)置成功,set trap應(yīng)該返回true,否則返回false(觸發(fā)TypeError)。
讓我們使用它來驗(yàn)證新值:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // to intercept property writing
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // added successfully
numbers.push(2); // added successfully
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
alert("This line is never reached (error in the line above)");
請注意:數(shù)組的內(nèi)置功能仍然有效!值是通過push添加的。當(dāng)添加值時,length屬性自動增加。我們的代理不會破壞任何東西。
我們不必重寫添加值的數(shù)組方法(如push和unshift等)來添加檢查,因?yàn)樗鼈冊趦?nèi)部使用由代理攔截的[[Set]]操作。
因此,代碼是干凈和簡潔的。
使用 ownKeys, getOwnPropertyDescriptor 進(jìn)行迭代
Object.keys, for...in 和迭代對象屬性的大多數(shù)其他方法使用[[OwnPropertyKeys]]內(nèi)部方法(被ownKeys陷阱截獲)來獲得屬性列表。
這些方法在細(xì)節(jié)上有所不同:
Object.getOwnPropertyNames(obj) 返回非符號鍵。
Object.getOwnPropertySymbols(obj) 返回符號鍵。
Object.keys/values()返回帶有可枚舉標(biāo)志的非符號鍵/值(屬性標(biāo)志在“屬性標(biāo)志和描述符”一文中解釋過)。
for..in 循環(huán)遍歷帶有enumerable標(biāo)志的非符號鍵和原型鍵。
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filters out _password
for(let key in user) alert(key); // name, then: age
// same effect on these methods:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
不過,如果返回對象中不存在的鍵,則返回Object.keys不會列出它:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
為什么?原因很簡單:Object.keys只返回帶有enumerable標(biāo)志的屬性。為了檢查它,它調(diào)用每個屬性的內(nèi)部方法[[GetOwnProperty]]來獲取它的描述符。這里,因?yàn)闆]有屬性,它的描述符是空的,沒有可枚舉標(biāo)志,所以它被跳過。
為對象。要返回一個屬性,我們需要它存在于對象中,并帶有enumerable標(biāo)志,或者可以攔截對[[GetOwnProperty]]的調(diào)用(陷阱getOwnPropertyDescriptor做了這個工作),并返回一個帶有enumerable: true的描述符。
let user = { };
user = new Proxy(user, {
ownKeys(target) { // called once to get a list of properties
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // called for every property
return {
enumerable: true,
configurable: true
/* ...other flags, probable "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
使用 deleteProperty 保護(hù)屬性
有一個廣泛的約定,即以下劃線為前綴的屬性和方法是內(nèi)部的。它們不應(yīng)該從對象外部訪問。
從技術(shù)上講,這是可能的:
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
讓我們使用代理來防止任何以_開頭的屬性的訪問。
我們需要陷阱:
讀取這樣的屬性時拋出錯誤,
設(shè)置為寫入時拋出錯誤,
刪除時拋出錯誤,
ownKeys排除以_開頭的屬性for..in和方法,如Object.keys。
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // to intercept property writing
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // to intercept property list
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" doesn't allow to read _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" doesn't allow to write _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" doesn't allow to delete _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" filters out _password
for(let key in user) alert(key); // name
請注意get陷阱的重要細(xì)節(jié),在(*)行:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
為什么我們需要一個函數(shù)來調(diào)用`value.bind(target)``?
原因是對象方法,如user.checkPassword(),必須能夠訪問_password:
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
使用 has in range
let range = {
start: 1,
end: 10
};
我們想使用in操作符來檢查一個數(shù)字是否在范圍內(nèi)。
has陷阱在調(diào)用中攔截。
has(target, property)
target — 是目標(biāo)對象,作為第一個參數(shù)傳遞給新代理,
property -屬性名稱
演示:
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
包裝函數(shù):apply
我們也可以用代理來封裝函數(shù)。
apply(target, thisArg, args)陷阱將調(diào)用代理作為函數(shù):
target是目標(biāo)對象(function是JavaScript中的對象),thisArg是this的值。args是一個參數(shù)列表。
function delay(f, ms) {
// return a wrapper that passes the call to f after the timeout
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// after this wrapping, calls to sayHi will be delayed for 3 seconds
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
正如我們已經(jīng)看到的,這基本上是可行的。包裝器函數(shù)(*)在超時后執(zhí)行調(diào)用。
但是包裝器函數(shù)不轉(zhuǎn)發(fā)屬性讀/寫操作或其他任何操作。包裝后,對原始函數(shù)的屬性的訪問將丟失,例如名稱、長度等:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length is the arguments count in its declaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments)
理要強(qiáng)大得多,因?yàn)樗鼘⑺袃?nèi)容轉(zhuǎn)發(fā)給目標(biāo)對象。
讓我們使用代理代替包裝函數(shù):
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)

