萬字長文: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]]hasin運算符[[Delete]]deletePropertydelete操作[[Call]]applyproxy 對象作爲函數被調用[[Construct]]constructnew操作[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf[[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf[[IsExtensible]]isExtensibleObject.isExtensible[[PreventExtensions]]preventExtensionsObject.preventExtensions[[DefineOwnProperty]]definePropertyObject.defineProperty, Object.defineProperties[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor,for..in,Object.keys/values/entries[[OwnPropertyKeys]]ownKeysObject.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] = valueReflect.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