萬字長文:Proxy 和 Reflect
Proxy 用於許多庫和某些瀏覽器框架。在本章中,我們將看到許多實際應用。
語法:
let proxy = new Proxy(target, handler)
-
target
—— 是要包裝的對象,可以是任何東西,包括函數。 -
handler
—— 代理配置:帶有 “鉤子”(“traps”,即攔截操作的方法)的對象。比如get
鉤子用於讀取target
屬性,set
鉤子寫入target
屬性等等。
對 proxy
進行操作,如果在 handler
中存在相應的鉤子,則它將運行,並且 Proxy 有機會對其進行處理,否則將直接對 target 進行處理。
首先,讓我們創建一個沒有任何鉤子的代理:
let target = {};
let proxy = new Proxy(target, {}); // 空的handler對象
proxy.test = 5; // 寫入 Proxy 對象 (1)
alert(target.test); // 返回 5,test屬性出現在了 target 上!
alert(proxy.test); // 還是 5,我們也可以從 proxy 對象讀取它 (2)
for(let key in proxy) alert(key); // 返回 test,迭代也正常工作! (3)
由於沒有鉤子,所有對 proxy
的操作都直接轉發給 target
。
-
寫入操作
proxy.test=
會將值寫入target
。 -
讀取操作
proxy.test
會從target
返回對應的值。 -
迭代
proxy
會從target
返回對應的值。
我們可以看到,沒有任何鉤子,proxy
是一個 target
的透明包裝.
Proxy
是一種特殊的 “奇異對象”。它沒有自己的屬性。如果 handler
爲空,則透明地將操作轉發給 target
。
要激活更多功能,讓我們添加鉤子。
我們可以用它們攔截什麼?
對於對象的大多數操作,JavaScript 規範中都有一個所謂的 “內部方法”,它描述了最底層的工作方式。例如 [[Get]]
,用於讀取屬性的內部方法, [[Set]]
,用於寫入屬性的內部方法,等等。這些方法僅在規範中使用,我們不能直接通過方法名調用它們。
Proxy 鉤子會攔截這些方法的調用。它們在代理規範和下表中列出。
對於每個內部方法,此表中都有一個鉤子:可用於添加到 new Proxy
時的 handler
參數中以攔截操作的方法名稱:
-
內部方法 Handler 方法 何時觸發
[[Get]]
get
讀取屬性[[Set]]
set
寫入屬性[[HasProperty]]
has
in
運算符[[Delete]]
deleteProperty
delete
操作[[Call]]
apply
proxy 對象作爲函數被調用[[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
Invariants
JavaScript 強制執行某些不變式————當必須由內部方法和鉤子來完成操作時。
其中大多數用於返回值:
-
[[Set]]
如果值已成功寫入,則必須返回true
,否則返回false
。 -
[[Delete]]
如果已成功刪除該值,則必須返回true
,否則返回false
。 -
…… 依此類推,我們將在下面的示例中看到更多內容。
還有其他一些不變量,例如:
[[GetPrototypeOf]]
, 應用於代理對象的,必須返回與[[GetPrototypeOf]]
應用於被代理對象相同的值。換句話說,讀取代理對象的原型必須始終返回被代理對象的原型。
鉤子可以攔截這些操作,但是必須遵循這些規則。
不變量確保語言功能的正確和一致的行爲。完整的不變量列表在規範,如果您不做奇怪的事情,就不會違反它們。
規範:
https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots)。
讓我們看看實際示例中的工作原理。
帶 “get” 鉤子的默認值
最常見的鉤子是用於讀取 / 寫入屬性。
要攔截讀取操作,handler
應該有 get(target, property, receiver)
方法。
讀取屬性時觸發該方法,參數如下:
-
target
—— 是目標對象,該對象作爲第一個參數傳遞給new Proxy
, -
property
—— 目標屬性名, -
receiver
—— 如果目標屬性是一個 getter 訪問器屬性,則receiver
就是本次讀取屬性所在的this
對象。通常,這就是proxy
對象本身(或者,如果我們從代理繼承,則是從該代理繼承的對象)。現在我們不需要此參數,因此稍後將對其進行詳細說明。
讓我們用 get
實現對象的默認值。
我們將創建一個對不存在的數組項返回 0 的數組。
通常,當人們嘗試獲取不存在的數組項時,他們會得到 undefined
, 但是我們會將常規數組包裝到代理中,以捕獲讀取操作並在沒有此類屬性的情況下返回 0
:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默認值
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (沒有這樣的元素)
如我們所見,使用 get
鉤子非常容易。
我們可以用 Proxy
來實現任何讀取默認值的邏輯。
想象一下,我們有一本詞典,上面有短語及其翻譯:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
現在,如果沒有短語,從 dictionary
讀取將返回 undefined
。但實際上,返回一個未翻譯短語通常比 undefined
要好。因此,讓我們在這種情況下返回一個未翻譯的短語,而不是 undefined
。
爲此,我們將包裝 dictionary
進一個攔截讀取操作的代理:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // 攔截讀取屬性操作
if (phrase in target) { //如果字典包含該短語
return target[phrase]; // 返回譯文
} else {
// 否則返回未翻譯的短語
return phrase;
}
}
});
// 在字典中查找任意短語!
// 最壞的情況也只是它們沒有被翻譯。
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy
請注意代理如何覆蓋變量:
dictionary = new Proxy(dictionary, ...);
代理應該在所有地方都完全替代了目標對象。目標對象被代理後,任何人都不應該再引用目標對象。否則很容易搞砸。
使用 “set” 鉤子進行驗證
假設我們想要一個專門用於數字的數組。如果添加了其他類型的值,則應該拋出一個錯誤。
當寫入屬性時 set
鉤子觸發。
set(target, property, value, receiver)
:
-
target
—— 是目標對象,該對象作爲第一個參數傳遞給new Proxy
, -
property
—— 目標屬性名稱, -
value
—— 目標屬性要設置的值, -
receiver
—— 與get
鉤子類似,僅與 setter 訪問器相關。
如果寫入操作成功,set
鉤子應該返回 true
,否則返回 false
(觸發 TypeError
)。
讓我們用它來驗證新值:
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)");
我們不必重寫諸如 push
和 unshift
等添加元素的數組方法,就可以在其中添加檢查,因爲在內部它們使用代理所攔截的 [[Set]]
操作。
因此,代碼簡潔明瞭。
別忘了返回 true
如上所述,要保持不變式。
對於 set
操作, 它必須在成功寫入時返回 true
。
如果我們忘記這樣做或返回任何 falsy 值,則該操作將觸發 TypeError
。
使用 “ownKeys” 和 “getOwnPropertyDescriptor” 進行迭代
Object.keys
,for..in
循環和大多數其他遍歷對象屬性的方法都使用 [[OwnPropertyKeys]]
內部方法(由 ownKeys
鉤子攔截) 來獲取屬性列表。
這些方法在細節上有所不同:
-
Object.getOwnPropertyNames(obj)
返回非 Symbol 鍵。 -
Object.getOwnPropertySymbols(obj)
返回 symbol 鍵。 -
Object.keys/values()
返回帶有enumerable
標記的非 Symbol 鍵值對(屬性標記在章節 屬性標誌和屬性描述符 有詳細描述). -
for..in
循環遍歷所有帶有enumerable
標記的非 Symbol 鍵,以及原型對象的鍵。
…… 但是所有這些都從該列表開始。
在下面的示例中,我們使用 ownKeys
鉤子攔截 for..in
對 user
的遍歷,還使用 Object.keys
和 Object.values
來跳過以下劃線 _
開頭的屬性:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 過濾掉 _password
for(let key in user) alert(key); // name,然後是 age
// 對這些方法同樣有效:
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
標記的屬性。爲了檢查它, 該方法會對每個屬性調用 [[GetOwnProperty]]
來獲得屬性描述符。在這裏,由於沒有屬性,其描述符爲空,沒有 enumerable
標記,因此它將略過。
爲了讓 Object.keys
返回一個屬性,我們要麼需要將該屬性及 enumerable
標記存入對象,或者我們可以攔截對它的調用 [[GetOwnProperty]]
(鉤子getOwnPropertyDescriptor
會執行此操作),並返回描述符 enumerable: true。
這是一個例子:
let user = { };
user = new Proxy(user, {
ownKeys(target) { // 一旦被調用,就返回一個屬性列表
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 被每個屬性調用
return {
enumerable: true,
configurable: true
/* 其他屬性,類似於 "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
讓我們再次注意:如果該屬性在對象中不存在,則我們只需要攔截 [[GetOwnProperty]]
。
具有 “deleteProperty” 和其他鉤子的受保護屬性
有一個普遍的約定,即下劃線 _
前綴的屬性和方法是內部的。不應從對象外部訪問它們。
從技術上講,這是可能的:
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
讓我們使用代理來防止對以 _
開頭的屬性的任何訪問。
我們需要以下鉤子:
-
get
讀取此類屬性時拋出錯誤, -
set
寫入屬性時拋出錯誤, -
deleteProperty
刪除屬性時拋出錯誤, -
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) { // 攔截寫入操作
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
for(let key in user) alert(key); // name
請注意在行 (*)
中 get
鉤子的重要細節:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
爲什麼我們需要一個函數調用 value.bind(target)
?
原因是對象方法(例如 user.checkPassword()
)必須能夠訪問 _password
:
user = {
// ...
checkPassword(value) {
//對象方法必須能讀取 _password
return value === this._password;
}
}
對 user.checkPassword()
的一個調用會調用代理對象 user
作爲 this
(點運算符之前的對象會成爲 this
),因此,當它嘗試訪問 this._password
時 get
鉤子將激活(它在讀取任何屬性時觸發)並拋出錯誤。
因此,我們在行 (*)
中將對象方法的上下文綁定到原始對象,target
。然後,它們將來的調用將使用 target
作爲 this
,不觸發任何鉤子。
該解決方案通常可行,但並不理想,因爲一種方法可能會將未代理的對象傳遞到其他地方,然後我們會陷入困境:原始對象在哪裏,代理的對象在哪裏?
此外,一個對象可能會被代理多次(多個代理可能會對該對象添加不同的 “調整”),並且如果我們將未包裝的對象傳遞給方法,則可能會產生意想不到的後果。
因此,在任何地方都不應使用這種代理。
類的私有屬性
現代 Javascript 引擎原生支持私有屬性,其以 #
作爲前綴。這在章節 私有的和受保護的屬性和方法 中有詳細描述。Proxy 並不是必需的。
但是,此類屬性有其自身的問題。特別是,它們是不可繼承的。
“In range” 及 “has” 鉤子
讓我們來看更多示例。
我們有一個 range 對象:
let range = {
start: 1,
end: 10
};
我們想使用 in
運算符來檢查數字是否在 range
範圍內。
該 has
鉤子攔截 in
調用。
has(target, property)
-
target
—— 是目標對象,作爲第一個參數傳遞給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
漂亮的語法糖,不是嗎?而且實現起來非常簡單。
包裝函數:"apply"
我們也可以將代理包裝在函數週圍。
apply(target, thisArg, args)
鉤子能使代理以函數的方式被調用:
-
target
是目標對象(函數是 JavaScript 中的對象) -
thisArg
是this
的值 -
args
是參數列表
例如,讓我們回想一下 delay(f, ms)
裝飾器,它是我們在 裝飾者模式,call/apply 一章中完成的。
在該章中,我們沒有用 proxy 來實現它。調用 delay(f, ms)
返回一個函數,該函數會將在 ms
毫秒後把所有調用轉發到 f
。
這是以前的基於函數的實現:
function delay(f, ms) {
// 返回一個超時後調用 f 函數的包裝器
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 這次包裝後,sayHi 在3秒後被調用
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (3秒後)
正如我們已經看到的那樣,大多數情況下都是可行的。包裝函數 (*)
在超時後執行調用。
但是包裝函數不會轉發屬性讀 / 寫操作或其他任何操作。包裝後,無法訪問原有函數的屬性,比如 name
,length
和其他:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (函數的 length 是其聲明中的參數個數)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (在包裝器聲明中,參數個數爲0)
Proxy
功能強大得多,因爲它將所有東西轉發到目標對象。
讓我們使用 Proxy
而不是包裝函數:
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” 操作到目標對象
sayHi("John"); // Hello, John! (3秒後)
結果是相同的,但現在不僅調用,而且代理上的所有操作都轉發到原始函數。所以 sayHi.length 在 (*)
行包裝後正確返回結果 (*)。
我們有一個 “更豐富” 的包裝器。
還存在其他鉤子:完整列表在本章的開頭。它們的使用模式與上述類似。
Reflect
Reflect
是一個內置對象,可簡化的創建 Proxy
。
以前的內部方法,比如[[Get]]
,[[Set]]
等等都只是規範,不能直接調用。
Reflect
對象使調用這些內部方法成爲可能。它的方法是內部方法的最小包裝。
這是 Reflect
執行相同操作和調用的示例:
-
操作
Reflect
調用 內部方法 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
允許我們使用函數(Reflect.construct
,Reflect.deleteProperty
,……)執行操作(new
,delete
,……)。這是一個有趣的功能,但是這裏還有一點很重要。
對於每個可被 Proxy
捕獲的內部方法,Reflect
都有一個對應的方法 Reflect,其名稱和參數與 Proxy
鉤子相同。
因此,我們可以用 Reflect
來將操作轉發到原始對象。
在此示例中,鉤子get
和 set
透明地(好像它們都不存在)將讀 / 寫操作轉發到對象,並顯示一條消息:
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; // shows "GET name"
user.name = "Pete"; // shows "SET
這裏:
-
Reflect.get
讀取一個對象屬性 -
Reflect.set
寫入對象屬性,成功返回true
,否則返回false
就是說,一切都很簡單:如果鉤子想要將調用轉發給對象,則只需使用相同的參數調用 Reflect.<method>
就足夠了。
在大多數情況下,我們可以不使用 Reflect
完成相同的事情,例如,使用Reflect.get(target, prop, receiver)
讀取屬性可以替換爲 target[prop]
。儘管有一些細微的差別。
代理一個 getter
讓我們看一個示例,說明爲什麼 Reflect.get
更好。我們還將看到爲什麼 get/set
有第四個參數 receiver
,而我們以前沒有使用過它。
我們有一個帶有一個 _name
屬性和一個 getter 的對象 user
。
這是一個 Proxy:
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
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"
};
// Expected: Admin
alert(admin.name); // 輸出:Guest (?!?)
-
當我們讀取
admin.name
,由於admin
對象自身沒有對應的的屬性,搜索將轉到其原型。 -
原型是
userProxy
。 -
從代理讀取
name
屬性時,get
鉤子會觸發並從原始對象返回target[prop]
屬性,在(*)
行當調用
target[prop]
時,若prop
是一個 getter,它將在this=target
上下文中運行其代碼。因此,結果是來自原始對象target
的this._name
即來自user
。
爲了解決這種情況,我們需要 get
鉤子的第三個參數 receiver
。它保證傳遞正確的 this
給 getter。在我們的情況下是 admin
。
如何爲 getter 傳遞上下文?對於常規函數,我們可以使用 call/apply
,但這是一個 getter,它不是 “被調用” 的,只是被訪問的。
Reflect.get
可以做到的。如果我們使用它,一切都會正常運行。
這是更正後的變體:
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
現在 receiver
,保留了對正確 this
的引用(即admin
)的引用,該引用將在 (*)
行中使用Reflect.get
傳遞給 getter。
我們可以將鉤子重寫得更短:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect
調用的命名方式與鉤子完全相同,並且接受相同的參數。它們是通過這種方式專門設計的。
因此, return Reflect...
會提供一個安全的提示程序來轉發操作,並確保我們不會忘記與此相關的任何內容。
Proxy 的侷限
代理提供了一種獨特的方法,可以在最底層更改或調整現有對象的行爲。但是,它並不完美。有侷限性。
內置對象:內部插槽(Internal slots)
許多內置對象,例如 Map
, Set
, Date
, Promise
等等都使用了所謂的 “內部插槽”。
它們類似於屬性,但僅限於內部使用,僅用於規範目的。例如, Map
將項目存儲在 [[MapData]]
中。內置方法直接訪問它們,而不通過 [[Get]]/[[Set]]
內部方法。所以 Proxy
不能攔截。
爲什麼要在意呢?他們是內部的!
好吧,這就是問題。在像這樣的內置對象被代理後,代理對象沒有這些內部插槽,因此內置方法將失敗。
例如:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
在內部,一個 Map
將所有數據存儲在其 [[MapData]]
內部插槽中。代理對象沒有這樣的插槽。內建方法 Map.prototype.set
方法試圖訪問內部屬性 this.[[MapData]]
,但由於 this=proxy
在 proxy
中不能找到它,只能失敗。
幸運的是,有一種解決方法:
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 (works!)
現在它可以正常工作,因爲 get
鉤子將函數屬性(例如 map.set
)綁定到目標對象(map
)本身。
與前面的示例不同,proxy.set(...)
內部 this
的值並不是 proxy
,而是原始對象 map
。因此,當set
鉤子的內部實現嘗試訪問 this.[[MapData]]
內部插槽時,它會成功。
Array
沒有內部插槽
一個明顯的例外:內置 Array
不使用內部插槽。那是出於歷史原因,因爲它出現於很久以前。
因此,代理數組時沒有這種問題。
私有字段
類的私有字段也會發生類似的情況。
例如,getName()
方法訪問私有的 #name
屬性並在代理後中斷:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
原因是專用字段是使用內部插槽實現的。JavaScript 訪問它們時不使用 [[Get]]/[[Set]]
。
在調用 getName()
時 this
的值是代理後的 user
,它沒有帶私有字段的插槽。
再次,bind 方法的解決方案使它恢復正常:
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
該解決方案有缺點,如前所述:將原始對象暴露給該方法,可能使其進一步傳遞並破壞其他代理功能。
Proxy != target
代理和原始對象是不同的對象。很自然吧?
因此,如果我們使用原始對象作爲鍵,然後對其進行代理,則找不到代理:
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
如我們所見,代理後,我們在 allUsers
中找不到 user
,因爲代理是一個不同的對象。
Proxy 無法攔截嚴格相等性測試 ===
Proxy 可以攔截許多運算符,例如 new(使用 construct
),in(使用 has
),delete(使用 deleteProperty
)等。
但是沒有辦法攔截對象的嚴格相等性測試。一個對象嚴格只等於自身,沒有其他值。
因此,比較對象是否相等的所有操作和內置類都會區分 target 和 proxy。這裏沒有透明的替代品。
可取消的 Proxy
一個可撤銷的代理是可以被禁用的代理。
假設我們有一個資源,並且想隨時關閉對該資源的訪問。
我們可以做的是將其包裝成可撤銷的代理,而沒有任何鉤子。這樣的代理會將操作轉發給對象,我們可以隨時將其禁用。
語法爲:
let {proxy, revoke} = Proxy.revocable(target, handler)
該調用返回一個帶有 proxy
和 revoke
函數的對象以將其禁用。
這是一個例子:
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// proxy 正常工作
alert(proxy.data); // Valuable data
// 之後某處調用
revoke();
// proxy 不再工作(已吊銷)
alert(proxy.data); // Error
調用 revoke()
會從代理中刪除對目標對象的所有內部引用,因此不再連接它們。之後可以對目標對象進行垃圾回收。
我們還可以將 revoke
存儲在 WeakMap
中,以便能夠通過代理對象輕鬆找到它:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..later in our code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Error(已吊銷)
這種方法的好處是我們不必隨身攜帶 revoke。我們可以在需要時從 map proxy
上獲取它。
此處我們使用WeakMap
而不是 Map
,因爲它不會阻止垃圾收集。如果代理對象變得 “無法訪問”(例如,沒有變量再引用它),則 WeakMap
允許將其與 它的 revoke
對象一起從內存中擦除,因爲我們不再需要它了。
總結
Proxy
是對象的包裝,將代理上的操作轉發到對象,並可以選擇捕獲其中的一些操作。
它可以包裝任何類型的對象,包括類和函數。
語法爲:
let proxy = new Proxy(target, {
/* traps */
});
…… 然後,我們應該在所有地方使用 proxy
而不是 target
。代理沒有自己的屬性或方法。如果提供了鉤子,它將捕獲操作,否則將其轉發給 target
對象。
我們可以捕獲:
-
讀取(
get
),寫入(set
),刪除(deleteProperty
)屬性(甚至是不存在的屬性)。 -
函數調用(
apply
鉤子)。 -
new
操作(construct
鉤子)。 -
許多其他操作(完整列表在本文開頭和 docs 中)。
這使我們能夠創建 “虛擬” 屬性和方法,實現默認值,可觀察對象,函數裝飾器等等。
我們還可以將對象多次包裝在不同的代理中,並用多個函數進行裝飾。
該 Reflect API 旨在補充 Proxy。對於任何 Proxy
鉤子,都有一個帶有相同參數的 Reflect
調用。我們應該使用它們將調用轉發給目標對象。
Proxy 有一些侷限:
-
內置對象具有 “內部插槽”,對這些對象的訪問無法被代理。請參閱上面的解決方法。
-
私有類字段也是如此,因爲它們是在內部使用插槽實現的。因此,代理方法的調用必須具有目標對象
this
才能訪問它們。 -
對象相等性測試
===
不能被攔截。 -
性能:基準測試取決於引擎,但通常使用最簡單的代理訪問屬性所需的時間要長几倍。實際上,這僅對某些 “瓶頸” 對象重要。
幾個小實例任務
讀取不存在的屬性時出錯
通常,嘗試讀取不存在的屬性會返回 undefined
。
創建一個代理,在嘗試讀取不存在的屬性時該代理拋出錯誤。
這可以幫助及早發現編程錯誤。
編寫一個接受 target
對象,並返回添加此方面功能的 proxy 的 wrap(target)
函數。
應滿足如下結果:
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
/* 你的代碼 */
});
}
user = wrap(user);
alert(user.name); // John
alert(user.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
用 - 1 索引訪問數組
在某些編程語言中,我們可以使用從結尾算起的負索引訪問數組元素。
像這樣:
let array = [1, 2, 3];
array[-1]; // 3,最後一個元素
array[-2]; // 2,從末尾開始向前移動一步
array[-3]; // 1,從末尾開始向前移動兩步
換句話說,array[-N]
與 array[array.length \- N]
相同。
創建一個 proxy 來實現該行爲。
那應該是這樣的:
let array = [1, 2, 3];
array = new Proxy(array, {
/* your code */
});
alert( array[-1] ); // 3
alert( array[-2] ); // 2
// 其他數組也應該適用於這個功能
解決方案
let array = [1, 2, 3];
array = new Proxy(array, {
get(target, prop, receiver) {
if (prop < 0) {
// even if we access it like arr[1]
// prop is a string, so need to convert it to number
prop = +prop + target.length;
}
return Reflect.get(target, prop, receiver);
}
});
alert(array[-1]); // 3
alert(array[-2]); // 2
Observable
創建一個通過返回代理 “使對象可觀察” 的 makeObservable(target)
函數。
它的工作方式如下:
function makeObservable(target) {
/* your code */
}
let user = {};
user = makeObservable(user);
user.observe((key, value) => {
alert(`SET ${key}=${value}`);
});
user.name = "John"; // alerts:設置 name 屬性爲 John
換句話說,makeObservable
返回的對象就像原始對象一樣,但是也具有將 handler
函數設置爲在任何屬性更改時都被調用的方法 observe(handler)
。
每當屬性更改時,都會使用屬性的名稱和值調用 handler(key, value)
。
P.S. 在此任務中,請僅注意寫入屬性。可以以類似方式實現其他操作。
解決方案
該解決方案包括兩部分:
-
無論
.observe(handler)
何時被調用,我們都需要在某個地方記住 handler,以便以後可以調用它。我們可以使用 Symbol 作爲屬性鍵,將 handler 直接存儲在對象中。 -
我們需要一個帶
set
鉤子的 proxy 來在發生任何更改時調用處理程序。
let handlers = Symbol('handlers');
function makeObservable(target) {
// 1. 初始化 handler 存儲數組
target[handlers] = [];
// 存儲 handler 函數到數組中以便於未來調用
target.observe = function(handler) {
this[handlers].push(handler);
};
// 2. 創建代理以處理更改
return new Proxy(target, {
set(target, property, value, receiver) {
let success = Reflect.set(...arguments); // 轉發寫入操作到目標對象
if (success) { // 如果設置屬性的時候沒有報錯
// 調用所有 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";
轉自: 王小醬
https://juejin.cn/post/6844904090116292616
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Y--hyizEZCIMtnZzdeO1tA