一個(gè) ?Proxy
? 對(duì)象包裝另一個(gè)對(duì)象并攔截諸如讀取/寫入屬性和其他操作,可以選擇自行處理它們,或者透明地允許該對(duì)象處理它們。
Proxy 被用于了許多庫(kù)和某些瀏覽器框架。在本文中,我們將看到許多實(shí)際應(yīng)用。
語(yǔ)法:
let proxy = new Proxy(target, handler)
target
? —— 是要包裝的對(duì)象,可以是任何東西,包括函數(shù)。handler
? —— 代理配置:帶有“捕捉器”(“traps”,即攔截操作的方法)的對(duì)象。比如 ?get
? 捕捉器用于讀取 ?target
? 的屬性,?set
? 捕捉器用于寫入 ?target
? 的屬性,等等。對(duì) proxy
進(jìn)行操作,如果在 handler
中存在相應(yīng)的捕捉器,則它將運(yùn)行,并且 Proxy 有機(jī)會(huì)對(duì)其進(jìn)行處理,否則將直接對(duì) target 進(jìn)行處理。
首先,讓我們創(chuàng)建一個(gè)沒(méi)有任何捕捉器的代理(Proxy):
let target = {};
let proxy = new Proxy(target, {}); // 空的 handler 對(duì)象
proxy.test = 5; // 寫入 proxy 對(duì)象 (1)
alert(target.test); // 5,test 屬性出現(xiàn)在了 target 中!
alert(proxy.test); // 5,我們也可以從 proxy 對(duì)象讀取它 (2)
for(let key in proxy) alert(key); // test,迭代也正常工作 (3)
由于沒(méi)有捕捉器,所有對(duì) proxy
的操作都直接轉(zhuǎn)發(fā)給了 target
。
proxy.test=
? 會(huì)將值寫入 ?target
?。proxy.test
? 會(huì)從 ?target
? 返回對(duì)應(yīng)的值。proxy
? 會(huì)從 ?target
? 返回對(duì)應(yīng)的值。我們可以看到,沒(méi)有任何捕捉器,proxy
是一個(gè) target
的透明包裝器(wrapper)。
Proxy
是一種特殊的“奇異對(duì)象(exotic object)”。它沒(méi)有自己的屬性。如果 handler
為空,則透明地將操作轉(zhuǎn)發(fā)給 target
。
要激活更多功能,讓我們添加捕捉器。
我們可以用它們攔截什么?
對(duì)于對(duì)象的大多數(shù)操作,JavaScript 規(guī)范中有一個(gè)所謂的“內(nèi)部方法”,它描述了最底層的工作方式。例如 [[Get]]
,用于讀取屬性的內(nèi)部方法,[[Set]]
,用于寫入屬性的內(nèi)部方法,等等。這些方法僅在規(guī)范中使用,我們不能直接通過(guò)方法名調(diào)用它們。
Proxy 捕捉器會(huì)攔截這些方法的調(diào)用。它們?cè)?nbsp;proxy 規(guī)范 和下表中被列出。
對(duì)于每個(gè)內(nèi)部方法,此表中都有一個(gè)捕捉器:可用于添加到 new Proxy
的 handler
參數(shù)中以攔截操作的方法名稱:
內(nèi)部方法 | Handler 方法 | 何時(shí)觸發(fā) |
---|---|---|
[[Get]]
|
get
|
讀取屬性 |
[[Set]]
|
set
|
寫入屬性 |
[[HasProperty]]
|
has
|
in 操作符 |
[[Delete]]
|
deleteProperty
|
delete 操作符 |
[[Call]]
|
apply
|
函數(shù)調(diào)用 |
[[Construct]]
|
construct
|
new 操作符 |
[[GetPrototypeOf]]
|
getPrototypeOf
|
Object.getPrototypeOf |
[[SetPrototypeOf]]
|
setPrototypeOf
|
Object.setPrototypeOf |
[[IsExtensible]]
|
isExtensible
|
Object.isExtensible |
[[PreventExtensions]]
|
preventExtensions
|
Object.preventExtensions |
[[DefineOwnProperty]]
|
defineProperty
|
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]]
|
getOwnPropertyDescriptor
|
Object.getOwnPropertyDescriptor, for..in , Object.keys/values/entries
|
[[OwnPropertyKeys]]
|
ownKeys
|
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in , Object.keys/values/entries
|
不變量(Invariant)
JavaScript 強(qiáng)制執(zhí)行某些不變量 —— 內(nèi)部方法和捕捉器必須滿足的條件。
其中大多數(shù)用于返回值:
- ?
[[Set]]
? 如果值已成功寫入,則必須返回 ?true
?,否則返回 ?false
?。- ?
[[Delete]]
? 如果已成功刪除該值,則必須返回 ?true
?,否則返回 ?false
?。- ……依此類推,我們將在下面的示例中看到更多內(nèi)容。
還有其他一些不變量,例如:
- 應(yīng)用于代理(proxy)對(duì)象的 ?
[[GetPrototypeOf]]
?,必須返回與應(yīng)用于被代理對(duì)象的 ?[[GetPrototypeOf]]
? 相同的值。換句話說(shuō),讀取代理對(duì)象的原型必須始終返回被代理對(duì)象的原型。捕捉器可以攔截這些操作,但是必須遵循上面這些規(guī)則。
不變量確保語(yǔ)言功能的正確和一致的行為。完整的不變量列表在 規(guī)范 中。如果你不做奇怪的事情,你可能就不會(huì)違反它們。
讓我們來(lái)看看它們是如何在實(shí)際示例中工作的。
最常見(jiàn)的捕捉器是用于讀取/寫入的屬性。
要攔截讀取操作,handler
應(yīng)該有 get(target, property, receiver)
方法。
讀取屬性時(shí)觸發(fā)該方法,參數(shù)如下:
target
? —— 是目標(biāo)對(duì)象,該對(duì)象被作為第一個(gè)參數(shù)傳遞給 ?new Proxy
?,property
? —— 目標(biāo)屬性名,receiver
? —— 如果目標(biāo)屬性是一個(gè) getter 訪問(wèn)器屬性,則 ?receiver
? 就是本次讀取屬性所在的 ?this
? 對(duì)象。通常,這就是 ?proxy
? 對(duì)象本身(或者,如果我們從 proxy 繼承,則是從該 proxy 繼承的對(duì)象)。現(xiàn)在我們不需要此參數(shù),因此稍后我們將對(duì)其進(jìn)行詳細(xì)介紹。讓我們用 get
來(lái)實(shí)現(xiàn)一個(gè)對(duì)象的默認(rèn)值。
我們將創(chuàng)建一個(gè)對(duì)不存在的數(shù)組項(xiàng)返回 0
的數(shù)組。
通常,當(dāng)人們嘗試獲取不存在的數(shù)組項(xiàng)時(shí),他們會(huì)得到 undefined
,但是我們?cè)谶@將常規(guī)數(shù)組包裝到代理(proxy)中,以捕獲讀取操作,并在沒(méi)有要讀取的屬性的時(shí)返回 0
:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默認(rèn)值
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0(沒(méi)有這個(gè)數(shù)組項(xiàng))
正如我們所看到的,使用 get
捕捉器很容易實(shí)現(xiàn)。
我們可以用 Proxy
來(lái)實(shí)現(xiàn)“默認(rèn)”值的任何邏輯。
想象一下,我們有一本詞典,上面有短語(yǔ)及其翻譯:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
現(xiàn)在,如果沒(méi)有我們要讀取的短語(yǔ),那么從 dictionary
讀取它將返回 undefined
。但實(shí)際上,返回一個(gè)未翻譯的短語(yǔ)通常比 undefined
要好。因此,讓我們?cè)谶@種情況下返回一個(gè)未翻譯的短語(yǔ)來(lái)替代 undefined
。
為此,我們將把 dictionary
包裝進(jìn)一個(gè)攔截讀取操作的代理:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // 攔截讀取屬性操作
if (phrase in target) { //如果詞典中有該短語(yǔ)
return target[phrase]; // 返回其翻譯
} else {
// 否則返回未翻譯的短語(yǔ)
return phrase;
}
}
});
// 在詞典中查找任意短語(yǔ)!
// 最壞的情況也只是它們沒(méi)有被翻譯。
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy(沒(méi)有被翻譯)
請(qǐng)注意:
請(qǐng)注意代理如何覆蓋變量:
dictionary = new Proxy(dictionary, ...);
代理應(yīng)該在所有地方都完全替代目標(biāo)對(duì)象。目標(biāo)對(duì)象被代理后,任何人都不應(yīng)該再引用目標(biāo)對(duì)象。否則很容易搞砸。
假設(shè)我們想要一個(gè)專門用于數(shù)字的數(shù)組。如果添加了其他類型的值,則應(yīng)該拋出一個(gè)錯(cuò)誤。
當(dāng)寫入屬性時(shí) set
捕捉器被觸發(fā)。
set(target, property, value, receiver)
:
target
? —— 是目標(biāo)對(duì)象,該對(duì)象被作為第一個(gè)參數(shù)傳遞給 ?new Proxy
?,property
? —— 目標(biāo)屬性名稱,value
? —— 目標(biāo)屬性的值,receiver
? —— 與 ?get
? 捕捉器類似,僅與 setter 訪問(wèn)器屬性相關(guān)。如果寫入操作(setting)成功,set
捕捉器應(yīng)該返回 true
,否則返回 false
(觸發(fā) TypeError
)。
讓我們用它來(lái)驗(yàn)證新值:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 攔截寫入屬性操作
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)
alert("This line is never reached (error in the line above)");
請(qǐng)注意:數(shù)組的內(nèi)建方法依然有效!值被使用 push
方法添加到數(shù)組。當(dāng)值被添加到數(shù)組后,數(shù)組的 length
屬性會(huì)自動(dòng)增加。我們的代理對(duì)象 proxy 不會(huì)破壞任何東西。
我們不必重寫諸如 push
和 unshift
等添加元素的數(shù)組方法,就可以在其中添加檢查,因?yàn)樵趦?nèi)部它們使用代理所攔截的 [[Set]]
操作。
因此,代碼簡(jiǎn)潔明了。
別忘了返回 ?
true
?如上所述,要保持不變量。
對(duì)于
set
操作,它必須在成功寫入時(shí)返回true
。
如果我們忘記這樣做,或返回任何假(falsy)值,則該操作將觸發(fā)
TypeError
。
Object.keys
,for..in
循環(huán)和大多數(shù)其他遍歷對(duì)象屬性的方法都使用內(nèi)部方法 [[OwnPropertyKeys]]
(由 ownKeys
捕捉器攔截) 來(lái)獲取屬性列表。
這些方法在細(xì)節(jié)上有所不同:
Object.getOwnPropertyNames(obj)
? 返回非 symbol 鍵。Object.getOwnPropertySymbols(obj)
? 返回 symbol 鍵。Object.keys/values()
? 返回帶有 ?enumerable
? 標(biāo)志的非 symbol 鍵/值(屬性標(biāo)志在 屬性標(biāo)志和屬性描述符 一章有詳細(xì)講解)。for..in
? 循環(huán)遍歷所有帶有 ?enumerable
? 標(biāo)志的非 symbol 鍵,以及原型對(duì)象的鍵。……但是所有這些都從該列表開(kāi)始。
在下面這個(gè)示例中,我們使用 ownKeys
捕捉器攔截 for..in
對(duì) user
的遍歷,并使用 Object.keys
和 Object.values
來(lái)跳過(guò)以下劃線 _
開(kāi)頭的屬性:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 過(guò)濾掉了 _password
for(let key in user) alert(key); // name,然后是 age
// 對(duì)這些方法的效果相同:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
到目前為止,它仍然有效。
盡管如此,但如果我們返回對(duì)象中不存在的鍵,Object.keys
并不會(huì)列出這些鍵:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
為什么?原因很簡(jiǎn)單:Object.keys
僅返回帶有 enumerable
標(biāo)志的屬性。為了檢查它,該方法會(huì)對(duì)每個(gè)屬性調(diào)用內(nèi)部方法 [[GetOwnProperty]]
來(lái)獲取 它的描述符(descriptor)。在這里,由于沒(méi)有屬性,其描述符為空,沒(méi)有 enumerable
標(biāo)志,因此它被略過(guò)。
為了讓 Object.keys
返回一個(gè)屬性,我們需要它要么存在于帶有 enumerable
標(biāo)志的對(duì)象,要么我們可以攔截對(duì) [[GetOwnProperty]]
的調(diào)用(捕捉器 getOwnPropertyDescriptor
可以做到這一點(diǎn)),并返回帶有 enumerable: true
的描述符。
這是關(guān)于此的一個(gè)例子:
let user = { };
user = new Proxy(user, {
ownKeys(target) { // 一旦要獲取屬性列表就會(huì)被調(diào)用
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 被每個(gè)屬性調(diào)用
return {
enumerable: true,
configurable: true
/* ...其他標(biāo)志,可能是 "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
讓我們?cè)俅巫⒁猓喝绻搶傩栽趯?duì)象中不存在,那么我們只需要攔截 [[GetOwnProperty]]
。
有一個(gè)普遍的約定,即以下劃線 ?_
? 開(kāi)頭的屬性和方法是內(nèi)部的。不應(yīng)從對(duì)象外部訪問(wèn)它們。
從技術(shù)上講,我們也是能訪問(wèn)到這樣的屬性的:
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
讓我們使用代理來(lái)防止對(duì)以 _
開(kāi)頭的屬性的任何訪問(wèn)。
我們將需要以下捕捉器:
get
? 讀取此類屬性時(shí)拋出錯(cuò)誤,set
? 寫入屬性時(shí)拋出錯(cuò)誤,deleteProperty
? 刪除屬性時(shí)拋出錯(cuò)誤,ownKeys
? 在使用 ?for..in
? 和像 ?Object.keys
? 這樣的的方法時(shí)排除以 ?_
? 開(kāi)頭的屬性。代碼如下:
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) { // 攔截屬性寫入
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // 攔截屬性刪除
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // 攔截讀取屬性列表
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" 不允許讀取 _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" 不允許寫入 _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" 不允許刪除 _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" 將 _password 過(guò)濾出去
for(let key in user) alert(key); // name
請(qǐng)注意在 (*)
行中 get
捕捉器的重要細(xì)節(jié):
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
為什么我們需要一個(gè)函數(shù)去調(diào)用 value.bind(target)
?
原因是對(duì)象方法(例如 user.checkPassword()
)必須能夠訪問(wèn) _password
:
user = {
// ...
checkPassword(value) {
//對(duì)象方法必須能讀取 _password
return value === this._password;
}
}
對(duì) user.checkPassword()
的調(diào)用會(huì)將被代理的對(duì)象 user
作為 this
(點(diǎn)符號(hào)之前的對(duì)象會(huì)成為 this
),因此,當(dāng)它嘗試訪問(wèn) this._password
時(shí),get
捕捉器將激活(在任何屬性讀取時(shí),它都會(huì)被觸發(fā))并拋出錯(cuò)誤。
因此,我們?cè)?nbsp;(*)
行中將對(duì)象方法的上下文綁定到原始對(duì)象 target
。然后,它們將來(lái)的調(diào)用將使用 target
作為 this
,不會(huì)觸發(fā)任何捕捉器。
該解決方案通??尚?,但并不理想,因?yàn)橐粋€(gè)方法可能會(huì)將未被代理的對(duì)象傳遞到其他地方,然后我們就會(huì)陷入困境:原始對(duì)象在哪里,被代理的對(duì)象在哪里?
此外,一個(gè)對(duì)象可能會(huì)被代理多次(多個(gè)代理可能會(huì)對(duì)該對(duì)象添加不同的“調(diào)整”),并且如果我們將未包裝的對(duì)象傳遞給方法,則可能會(huì)產(chǎn)生意想不到的后果。
因此,在任何地方都不應(yīng)使用這種代理。
類的私有屬性
現(xiàn)代 JavaScript 引擎原生支持 class 中的私有屬性,這些私有屬性以
#
為前綴。它們?cè)?nbsp;私有的和受保護(hù)的屬性和方法 一章中有詳細(xì)描述。無(wú)需代理(proxy)。
但是,此類屬性有其自身的問(wèn)題。特別是,它們是不可繼承的。
讓我們來(lái)看更多示例。
我們有一個(gè) range 對(duì)象:
let range = {
start: 1,
end: 10
};
我們想使用 in
操作符來(lái)檢查一個(gè)數(shù)字是否在 range
范圍內(nèi)。
has
捕捉器會(huì)攔截 in
調(diào)用。
?has(target, property)
?
target
? —— 是目標(biāo)對(duì)象,被作為第一個(gè)參數(shù)傳遞給 ?new Proxy
?,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
漂亮的語(yǔ)法糖,不是嗎?而且實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單。
我們也可以將代理(proxy)包裝在函數(shù)周圍。
apply(target, thisArg, args)
捕捉器能使代理以函數(shù)的方式被調(diào)用:
target
? 是目標(biāo)對(duì)象(在 JavaScript 中,函數(shù)就是一個(gè)對(duì)象),thisArg
? 是 ?this
? 的值。args
? 是參數(shù)列表。例如,讓我們回憶一下我們?cè)?nbsp;裝飾器模式和轉(zhuǎn)發(fā),call/apply 一章中所講的 delay(f, ms)
裝飾器。
在該章中,我們沒(méi)有用 proxy 來(lái)實(shí)現(xiàn)它。調(diào)用 delay(f, ms)
會(huì)返回一個(gè)函數(shù),該函數(shù)會(huì)在 ms
毫秒后把所有調(diào)用轉(zhuǎn)發(fā)給 f
。
這是以前的基于函數(shù)的實(shí)現(xiàn):
function delay(f, ms) {
// 返回一個(gè)包裝器(wrapper),該包裝器將在時(shí)間到了的時(shí)候?qū)⒄{(diào)用轉(zhuǎn)發(fā)給函數(shù) f
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 在進(jìn)行這個(gè)包裝后,sayHi 函數(shù)會(huì)被延遲 3 秒后被調(diào)用
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
正如我們所看到的那樣,大多數(shù)情況下它都是可行的。包裝函數(shù) (*)
在到達(dá)延遲的時(shí)間后后執(zhí)行調(diào)用。
但是包裝函數(shù)不會(huì)轉(zhuǎn)發(fā)屬性讀取/寫入操作或者任何其他操作。進(jìn)行包裝后,就失去了對(duì)原始函數(shù)屬性的訪問(wèn),例如 name
,length
和其他屬性:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1(函數(shù)的 length 是函數(shù)聲明中的參數(shù)個(gè)數(shù))
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0(在包裝器聲明中,參數(shù)個(gè)數(shù)為 0)
Proxy
的功能要強(qiáng)大得多,因?yàn)樗梢詫⑺袞|西轉(zhuǎn)發(fā)到目標(biāo)對(duì)象。
讓我們使用 Proxy
來(lái)替換掉包裝函數(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 將“獲取 length”的操作轉(zhuǎn)發(fā)給目標(biāo)對(duì)象
sayHi("John"); // Hello, John!(3 秒后)
結(jié)果是相同的,但現(xiàn)在不僅僅調(diào)用,而且代理上的所有操作都能被轉(zhuǎn)發(fā)到原始函數(shù)。所以在 (*)
行包裝后的 sayHi.length
會(huì)返回正確的結(jié)果。
我們得到了一個(gè)“更豐富”的包裝器。
還存在其他捕捉器:完整列表在本文的開(kāi)頭。它們的使用模式與上述類似。
Reflect
是一個(gè)內(nèi)建對(duì)象,可簡(jiǎn)化 Proxy
的創(chuàng)建。
前面所講過(guò)的內(nèi)部方法,例如 [[Get]]
和 [[Set]]
等,都只是規(guī)范性的,不能直接調(diào)用。
Reflect
對(duì)象使調(diào)用這些內(nèi)部方法成為了可能。它的方法是內(nèi)部方法的最小包裝。
以下是執(zhí)行相同操作和 Reflect
調(diào)用的示例:
操作 | Reflect 調(diào)用 |
內(nèi)部方法 |
---|---|---|
obj[prop]
|
Reflect.get(obj, prop)
|
[[Get]]
|
obj[prop] = value
|
Reflect.set(obj, prop, value)
|
[[Set]]
|
delete obj[prop]
|
Reflect.deleteProperty(obj, prop)
|
[[Delete]]
|
new F(value)
|
Reflect.construct(F, value)
|
[[Construct]]
|
… | … | … |
例如:
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
尤其是,Reflect
允許我們將操作符(new
,delete
,……)作為函數(shù)(Reflect.construct
,Reflect.deleteProperty
,……)執(zhí)行調(diào)用。這是一個(gè)有趣的功能,但是這里還有一點(diǎn)很重要。
對(duì)于每個(gè)可被 Proxy
捕獲的內(nèi)部方法,在 Reflect
中都有一個(gè)對(duì)應(yīng)的方法,其名稱和參數(shù)與 Proxy
捕捉器相同。
所以,我們可以使用 Reflect
來(lái)將操作轉(zhuǎn)發(fā)給原始對(duì)象。
在下面這個(gè)示例中,捕捉器 get
和 set
均透明地(好像它們都不存在一樣)將讀取/寫入操作轉(zhuǎn)發(fā)到對(duì)象,并顯示一條消息:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // 顯示 "GET name"
user.name = "Pete"; // 顯示 "SET name=Pete"
這里:
Reflect.get
? 讀取一個(gè)對(duì)象屬性。Reflect.set
? 寫入一個(gè)對(duì)象屬性,如果寫入成功則返回 ?true
?,否則返回 ?false
?。這樣,一切都很簡(jiǎn)單:如果一個(gè)捕捉器想要將調(diào)用轉(zhuǎn)發(fā)給對(duì)象,則只需使用相同的參數(shù)調(diào)用 Reflect.<method>
就足夠了。
在大多數(shù)情況下,我們可以不使用 Reflect
完成相同的事情,例如,用于讀取屬性的 Reflect.get(target, prop, receiver)
可以被替換為 target[prop]
。盡管有一些細(xì)微的差別。
讓我們看一個(gè)示例,來(lái)說(shuō)明為什么 Reflect.get
更好。此外,我們還將看到為什么 get/set
有第三個(gè)參數(shù) receiver
,而且我們之前從來(lái)沒(méi)有使用過(guò)它。
我們有一個(gè)帶有 _name
屬性和 getter 的對(duì)象 user
。
這是對(duì) user
對(duì)象的一個(gè)代理:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
其 get
捕捉器在這里是“透明的”,它返回原來(lái)的屬性,不會(huì)做任何其他的事。這對(duì)于我們的示例而言就足夠了。
一切似乎都很好。但是讓我們將示例變得稍微復(fù)雜一點(diǎn)。
另一個(gè)對(duì)象 admin
從 user
繼承后,我們可以觀察到錯(cuò)誤的行為:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// 期望輸出:Admin
alert(admin.name); // 輸出:Guest (?!?)
讀取 admin.name
應(yīng)該返回 "Admin"
,而不是 "Guest"
!
發(fā)生了什么?或許我們?cè)诶^承方面做錯(cuò)了什么?
但是,如果我們移除代理,那么一切都會(huì)按預(yù)期進(jìn)行。
問(wèn)題實(shí)際上出在代理中,在 (*)
行。
admin.name
? 時(shí),由于 ?admin
? 對(duì)象自身沒(méi)有對(duì)應(yīng)的的屬性,搜索將轉(zhuǎn)到其原型。userProxy
?。name
? 屬性時(shí),?get
? 捕捉器會(huì)被觸發(fā),并從原始對(duì)象返回 ?target[prop]
? 屬性,在 ?(*)
? 行。當(dāng)調(diào)用 ?target[prop]
? 時(shí),若 ?prop
? 是一個(gè) getter,它將在 ?this=target
? 上下文中運(yùn)行其代碼。因此,結(jié)果是來(lái)自原始對(duì)象 ?target
? 的 ?this._name
?,即來(lái)自 ?user
?。
為了解決這種情況,我們需要 get
捕捉器的第三個(gè)參數(shù) receiver
。它保證將正確的 this
傳遞給 getter。在我們的例子中是 admin
。
如何把上下文傳遞給 getter?對(duì)于一個(gè)常規(guī)函數(shù),我們可以使用 call/apply
,但這是一個(gè) getter,它不能“被調(diào)用”,只能被訪問(wèn)。
Reflect.get
可以做到。如果我們使用它,一切都會(huì)正常運(yùn)行。
這是更正后的變體:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
現(xiàn)在 receiver
保留了對(duì)正確 this
的引用(即 admin
),該引用是在 (*)
行中被通過(guò) Reflect.get
傳遞給 getter 的。
我們可以把捕捉器重寫得更短:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect
調(diào)用的命名與捕捉器的命名完全相同,并且接受相同的參數(shù)。它們是以這種方式專門設(shè)計(jì)的。
因此,return Reflect...
提供了一個(gè)安全的方式,可以輕松地轉(zhuǎn)發(fā)操作,并確保我們不會(huì)忘記與此相關(guān)的任何內(nèi)容。
代理提供了一種獨(dú)特的方法,可以在最底層更改或調(diào)整現(xiàn)有對(duì)象的行為。但是,它并不完美。有局限性。
許多內(nèi)建對(duì)象,例如 Map
,Set
,Date
,Promise
等,都使用了所謂的“內(nèi)部插槽”。
它們類似于屬性,但僅限于內(nèi)部使用,僅用于規(guī)范目的。例如,Map
將項(xiàng)目(item)存儲(chǔ)在 [[MapData]]
中。內(nèi)建方法可以直接訪問(wèn)它們,而不通過(guò) [[Get]]/[[Set]]
內(nèi)部方法。所以 Proxy
無(wú)法攔截它們。
為什么要在意這些呢?畢竟它們是內(nèi)部的!
好吧,問(wèn)題在這兒。在類似這樣的內(nèi)建對(duì)象被代理后,代理對(duì)象沒(méi)有這些內(nèi)部插槽,因此內(nèi)建方法將會(huì)失敗。
例如:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
在內(nèi)部,一個(gè) Map
將所有數(shù)據(jù)存儲(chǔ)在其 [[MapData]]
內(nèi)部插槽中。代理對(duì)象沒(méi)有這樣的插槽。內(nèi)建方法 Map.prototype.set
方法試圖訪問(wèn)內(nèi)部屬性 this.[[MapData]]
,但由于 this=proxy
,在 proxy
中無(wú)法找到它,只能失敗。
幸運(yùn)的是,這有一種解決方法:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1(工作了?。?/code>
現(xiàn)在它正常工作了,因?yàn)?nbsp;get
捕捉器將函數(shù)屬性(例如 map.set
)綁定到了目標(biāo)對(duì)象(map
)本身。
與前面的示例不同,proxy.set(...)
內(nèi)部 this
的值并不是 proxy
,而是原始的 map
。因此,當(dāng)set
捕捉器的內(nèi)部實(shí)現(xiàn)嘗試訪問(wèn) this.[[MapData]]
內(nèi)部插槽時(shí),它會(huì)成功。
?
Array
? 沒(méi)有內(nèi)部插槽一個(gè)值得注意的例外:內(nèi)建
Array
沒(méi)有使用內(nèi)部插槽。那是出于歷史原因,因?yàn)樗霈F(xiàn)于很久以前。
所以,代理數(shù)組時(shí)沒(méi)有這種問(wèn)題。
類的私有字段也會(huì)發(fā)生類似的情況。
例如,getName()
方法訪問(wèn)私有的 #name
屬性,并在代理后中斷:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
原因是私有字段是通過(guò)內(nèi)部插槽實(shí)現(xiàn)的。JavaScript 在訪問(wèn)它們時(shí)不使用 [[Get]]/[[Set]]
。
在調(diào)用 getName()
時(shí),this
的值是代理后的 user
,它沒(méi)有帶有私有字段的插槽。
再次,帶有 bind
方法的解決方案使它恢復(fù)正常:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
如前所述,該解決方案也有缺點(diǎn):它將原始對(duì)象暴露給該方法,可能使其進(jìn)一步傳遞并破壞其他代理功能。
代理和原始對(duì)象是不同的對(duì)象。這很自然,對(duì)吧?
所以,如果我們使用原始對(duì)象作為鍵,然后對(duì)其進(jìn)行代理,之后卻無(wú)法找到代理了:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
如我們所見(jiàn),進(jìn)行代理后,我們?cè)?nbsp;allUsers
中找不到 user
,因?yàn)榇硎且粋€(gè)不同的對(duì)象。
Proxy 無(wú)法攔截嚴(yán)格相等性檢查 ?
===
?Proxy 可以攔截許多操作符,例如
new
(使用construct
),in
(使用has
),delete
(使用deleteProperty
)等。
但是沒(méi)有辦法攔截對(duì)于對(duì)象的嚴(yán)格相等性檢查。一個(gè)對(duì)象只嚴(yán)格等于其自身,沒(méi)有其他值。
因此,比較對(duì)象是否相等的所有操作和內(nèi)建類都會(huì)區(qū)分對(duì)象和代理。這里沒(méi)有透明的替代品。
一個(gè) 可撤銷 的代理是可以被禁用的代理。
假設(shè)我們有一個(gè)資源,并且想隨時(shí)關(guān)閉對(duì)該資源的訪問(wèn)。
我們可以做的是將它包裝成可一個(gè)撤銷的代理,沒(méi)有任何捕捉器。這樣的代理會(huì)將操作轉(zhuǎn)發(fā)給對(duì)象,并且我們可以隨時(shí)將其禁用。
語(yǔ)法為:
let {proxy, revoke} = Proxy.revocable(target, handler)
該調(diào)用返回一個(gè)帶有 proxy
和 revoke
函數(shù)的對(duì)象以將其禁用。
這是一個(gè)例子:
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// 將 proxy 傳遞到其他某處,而不是對(duì)象...
alert(proxy.data); // Valuable data
// 稍后,在我們的代碼中
revoke();
// proxy 不再工作(revoked)
alert(proxy.data); // Error
對(duì) revoke()
的調(diào)用會(huì)從代理中刪除對(duì)目標(biāo)對(duì)象的所有內(nèi)部引用,因此它們之間再無(wú)連接。
最初,revoke
與 proxy
是分開(kāi)的,因此我們可以傳遞 proxy
,同時(shí)將 revoke
留在當(dāng)前范圍內(nèi)。
我們也可以通過(guò)設(shè)置 proxy.revoke = revoke
來(lái)將 revoke
綁定到 proxy
。
另一種選擇是創(chuàng)建一個(gè) WeakMap
,其中 proxy
作為鍵,相應(yīng)的 revoke
作為值,這樣可以輕松找到 proxy
所對(duì)應(yīng)的 revoke
:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ...我們代碼中的其他位置...
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Error(revoked)
此處我們使用 WeakMap
而不是 Map
,因?yàn)樗粫?huì)阻止垃圾回收。如果一個(gè)代理對(duì)象變得“不可訪問(wèn)”(例如,沒(méi)有變量再引用它),則 WeakMap
允許將其與它的 revoke
一起從內(nèi)存中清除,因?yàn)槲覀儾辉傩枰恕?
Proxy
是對(duì)象的包裝器,將代理上的操作轉(zhuǎn)發(fā)到對(duì)象,并可以選擇捕獲其中一些操作。
它可以包裝任何類型的對(duì)象,包括類和函數(shù)。
語(yǔ)法為:
let proxy = new Proxy(target, {
/* trap */
});
……然后,我們應(yīng)該在所有地方使用 proxy
而不是 target
。代理沒(méi)有自己的屬性或方法。如果提供了捕捉器(trap),它將捕獲操作,否則會(huì)將其轉(zhuǎn)發(fā)給 target
對(duì)象。
我們可以捕獲:
get
?),寫入(?set
?),刪除(?deleteProperty
?)屬性(甚至是不存在的屬性)。apply
? 捕捉器)。new
? 操作(?construct
? 捕捉器)。這使我們能夠創(chuàng)建“虛擬”屬性和方法,實(shí)現(xiàn)默認(rèn)值,可觀察對(duì)象,函數(shù)裝飾器等。
我們還可以將對(duì)象多次包裝在不同的代理中,并用多個(gè)各個(gè)方面的功能對(duì)其進(jìn)行裝飾。
Reflect API 旨在補(bǔ)充 Proxy。對(duì)于任意 Proxy
捕捉器,都有一個(gè)帶有相同參數(shù)的 Reflect
調(diào)用。我們應(yīng)該使用它們將調(diào)用轉(zhuǎn)發(fā)給目標(biāo)對(duì)象。
Proxy 有一些局限性:
this
? 才能訪問(wèn)它們。===
? 無(wú)法被攔截。通常,嘗試讀取不存在的屬性會(huì)返回 ?undefined
?。
創(chuàng)建一個(gè)代理,在嘗試讀取不存在的屬性時(shí),該代理拋出一個(gè)錯(cuò)誤。
這可以幫助及早發(fā)現(xiàn)編程錯(cuò)誤。
編寫一個(gè)函數(shù) wrap(target)
,該函數(shù)接受一個(gè) target
對(duì)象,并返回添加此方面功能的代理(proxy)。
其工作方式應(yīng)如下:
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
/* 你的代碼 */
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist: "age"
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
} else {
throw new ReferenceError(`Property doesn't exist: "${prop}"`)
}
}
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist: "age"
在某些編程語(yǔ)言中,我們可以使用從尾端算起的負(fù)值索引訪問(wèn)數(shù)組元素。
像這樣:
let array = [1, 2, 3];
array[-1]; // 3,最后一個(gè)元素
array[-2]; // 2,從尾端開(kāi)始向前移動(dòng)一步
array[-3]; // 1,從尾端開(kāi)始向前移動(dòng)兩步
換句話說(shuō),array[-N]
與 array[array.length - N]
相同。
創(chuàng)建一個(gè) proxy 來(lái)實(shí)現(xiàn)該行為。
其工作方式應(yīng)如下:
let array = [1, 2, 3];
array = new Proxy(array, {
/* 你的代碼 */
});
alert( array[-1] ); // 3
alert( array[-2] ); // 2
// 其他數(shù)組功能應(yīng)保持“原樣”
let array = [1, 2, 3];
array = new Proxy(array, {
get(target, prop, receiver) {
if (prop < 0) {
// 即使我們像 arr[1] 這樣訪問(wèn)它
// prop 是一個(gè)字符串,所以我們需要將其轉(zhuǎn)換成數(shù)字
prop = +prop + target.length;
}
return Reflect.get(target, prop, receiver);
}
});
alert(array[-1]); // 3
alert(array[-2]); // 2
創(chuàng)建一個(gè)函數(shù) ?makeObservable(target)
?,該函數(shù)通過(guò)返回一個(gè)代理“使得對(duì)象可觀察”。
其工作方式如下:
function makeObservable(target) {
/* 你的代碼 */
}
let user = {};
user = makeObservable(user);
user.observe((key, value) => {
alert(`SET ${key}=${value}`);
});
user.name = "John"; // alerts: SET name=John
換句話說(shuō),makeObservable
返回的對(duì)象就像原始對(duì)象一樣,但是具有 observe(handler)
方法,該方法可以將 handler
函數(shù)設(shè)置為在任何屬性被更改時(shí),都會(huì)被調(diào)用的函數(shù)。
每當(dāng)有屬性被更改時(shí),都會(huì)使用屬性的名稱和屬性值調(diào)用 handler(key, value)
函數(shù)。
P.S. 在本任務(wù)中,你可以只關(guān)注屬性寫入。其他的操作可以通過(guò)類似的方式實(shí)現(xiàn)。
該解決方案包括兩部分:
.observe(handler)
? 何時(shí)被調(diào)用,我們都需要在某個(gè)地方記住 handler,以便以后可以調(diào)用它。我們可以使用 Symbol 作為屬性鍵,將 handler 直接存儲(chǔ)在對(duì)象中。
set
? 陷阱的 proxy 來(lái)在發(fā)生任何更改時(shí)調(diào)用 handler。let handlers = Symbol('handlers');
function makeObservable(target) {
// 1. 初始化 handler 存儲(chǔ)
target[handlers] = [];
// 將 handler 函數(shù)存儲(chǔ)到數(shù)組中,以便于之后調(diào)用
target.observe = function(handler) {
this[handlers].push(handler);
};
// 2. 創(chuàng)建一個(gè) proxy 以處理更改
return new Proxy(target, {
set(target, property, value, receiver) {
let success = Reflect.set(...arguments); // 將操作轉(zhuǎn)發(fā)給對(duì)象
if (success) { // 如果在設(shè)置屬性時(shí)沒(méi)有出現(xiàn) error
// 調(diào)用所有 handler
target[handlers].forEach(handler => handler(property, value));
}
return success;
}
});
}
let user = {};
user = makeObservable(user);
user.observe((key, value) => {
alert(`SET ${key}=${value}`);
});
user.name = "John";
更多建議: