
英文 | https://javascript.plainenglish.io/why-proxies-in-javascript-are-fantastic-db100ddc10a0
翻譯 | 楊小愛
什麼是Proxy?它究竟是做什麼的?在解釋這一點之前,讓我們看一個真實世界的開發例子。
我們每個人在日常生活中都有很多事情要做,比如看郵件、收快遞等等。有時我們可能會感到有點焦慮:我們的郵件清單上有很多垃圾郵件,需要花費大量時間來篩選它們;收到的貨物可能含有恐怖分子安放的炸彈,威脅到我們的安全。
那麼,這時你可能需要一個忠誠的管家,您希望管家幫助您執行以下操作:在您開始閱讀之前讓其檢查您的收件箱并删除所有垃圾郵件;當您收到包裹時,請它用專業裝置檢查包裹,確定裡面沒有炸彈。
在上面的例子中,管家就是我們的代理,當我們試圖做某事時,管家為我們做了一些額外的事情。
現在讓我們回到 JavaScript,我們知道 JavaScript 是一種面向對象的程式設計語言,沒有對象我們就無法編寫代碼。但是 JavaScript 對象總是裸奔的,你可以用它們做任何事情。很多時候,這會降低我們的代碼安全性。
是以在 ECMAScript2015 中引入了 Proxy 功能。有了Proxy,我們可以為物件找到忠實的管家,幫助我們增強物件原有的功能。
在最基本的層面上,使用 Proxy 的文法看起來像這樣:
// This is a normal object
let obj = {a: 1, b:2}
// Configure obj with a housekeeper using Proxy
let objProxy = new Proxy(obj, handler)
這隻是代碼的示例,因為我們還沒有寫handler,是以這段代碼暫時還不能正常運作。
對于一個人來說,我們可能有閱讀郵件、取件等操作,管家可以為我們做這些。對于一個對象,我們可以讀取屬性、設定屬性等等,這些也可以通過代理對象來增強。
在處理程式中,我們可以列出我們想要代理的操作。例如,如果我們想在擷取對象屬性的同時在控制台中列印出一條語句,我們可以這樣寫:
let obj = {a: 1, b:2}
// Use Proxy syntax to find a housekeeper for the object
let objProxy = new Proxy(obj, {
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[property]
}
})
在上面的示例中,我們的處理程式是:
{
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[propery]
}
當我們嘗試讀取對象的屬性時,get 函數就會執行。
get 函數可以接受三個參數:
- item :它是對象本身。
- proerty :您要讀取的屬性的名稱。
- itemProxy :它是我們剛剛建立的管家對象。
你可能已經在其他地方閱讀過有關 Proxy 的教程,并且您會注意到我對參數的命名與它們不同。我這樣做是為了更接近我之前的示例,以幫助你了解。我希望它對你有用。
那麼get函數的傳回值就是讀取這個屬性的結果。因為我們還不想改變任何東西,是以我們隻傳回原始對象的屬性值。
如果需要,我們也可以更改結果。例如,我們可以這樣做:
let obj = {a: 1, b:2}
let objProxy = new Proxy(obj, {
get: function(item, property, itemProxy){
console.log(`You are getting the value of '${property}' property`)
return item[property] * 2
}
})
以下是讀取它的屬性的結果:
我們将跟進實際示例來說明此技巧的實際用途。
除了攔截對屬性的讀取,我們還可以攔截對屬性的修改。像這樣:
let obj = {a: 1, b:2}
let objProxy = new Proxy(obj, {
set: function(item, property, value, itemProxy){
console.log(`You are setting '${value}' to '${property}' property`)
item[property] = value
}
})
當我們嘗試設定對象屬性的值時,會觸發 set 函數。
因為我們在設定屬性值時需要傳遞一個額外的值,是以上面的 set 函數比 get 函數多了一個參數。
除了攔截對屬性的讀取和修改外,Proxy 總共可以攔截對對象的 13 種操作。
他們是:
1. get(item, propKey, itemProxy):攔截對象屬性的讀取操作,如obj.a和ojb['b']
2. set(item, propKey, value, itemProxy):攔截對象屬性的設定操作,如 obj.a = 1 。
3. has(item, propKey):攔截objProxy中propKey的操作,傳回一個布爾值。
4. deleteProperty(item, propKey):攔截delete proxy[propKey]的操作,傳回一個布爾值。
5. ownKeys(item):攔截Object.getOwnPropertyNames(proxy),Object.getOwnPropertySymbols(proxy),Object.keys(proxy),for...in等操作,傳回一個數組。該方法傳回目标對象自身所有屬性的屬性名,而 Object.keys() 的傳回結果隻包含目标對象自身的可枚舉屬性。
6. getOwnPropertyDescriptor(item, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey)的操作,傳回屬性的描述符。
7. defineProperty(item, propKey, propDesc):攔截這些操作:Object.defineProperty(proxy, propKey, propDesc),Object.defineProperties(proxy, propDescs),傳回一個布爾值。
8. preventExtensions(item):攔截Object.preventExtensions(proxy)的操作,傳回一個布爾值。
9. getPrototypeOf(item):攔截Object.getPrototypeOf(proxy)的操作,傳回一個對象。
10. isExtensible(item):攔截Object.isExtensible(proxy)的操作,傳回一個布爾值。
11. setPrototypeOf(item, proto):攔截Object.setPrototypeOf(proxy, proto)的操作,傳回一個布爾值。
12. 如果目标對象是一個函數,還有兩個額外的操作要intercept.s
13. apply(item, object, args):攔截函數調用操作,如proxy(...args),proxy.call(object, ...args),proxy.apply(...)。
14. constructor(item, args):攔截Proxy執行個體調用的操作作為構造函數,如new proxy(...args)。
有些攔截不常用,我就不細說了。現在讓我們進入現實世界的例子,看看 Proxy 可以為我們做什麼。
1、實作數組的負索引
我們知道其他一些程式設計語言,例如 Python,支援對數組的負索引通路。
負索引以數組的最後一個位置為起點并向前計數。如:
- arr[-1] 是數組的最後一個元素。
- arr[-3] 是數組中倒數第三個元素。
許多人認為這是一個非常有用的功能,但不幸的是,JavaScript 目前不支援負索引文法。
但是 JavaScript 中強大的 Proxy 給了我們元程式設計的能力。
我們可以将數組包裝為 Proxy 對象。當使用者試圖通路一個負數索引時,我們可以通過 Proxy 的 get 方法攔截這個操作。然後根據之前定義的規則将負索引轉換為正索引,通路就完成了。
讓我們從一個基本操作開始:攔截對數組屬性的讀取。
function negativeArray(array) {
return new Proxy(array, {
get: function(item, propKey){
console.log(propKey)
return item[propKey]
}
})
}
上面的函數可以包裝一個數組,讓我們看看它是如何使用的。
如您所見,我們對數組屬性的讀取确實被攔截了。
請注意:JavaScript 中的對象隻能有一個字元串或符号類型的鍵,當我們寫 arr[1] 時,它實際上是在通路 arr['1'] ,鍵是字元串“1”,而不是數字 1。
是以現在我們需要做的是:當使用者試圖通路一個屬性是數組的索引時,發現它是一個負索引,然後進行相應的攔截和處理;如果屬性不是索引,或者索引是正數,我們什麼也不做。
結合以上需求,我們可以編寫如下模闆代碼。
function negativeArray(array) {
return new Proxy(array, {
get: function(target, propKey){
if(/** the propKey is a negative index */){
// translate the negative index to positive
}
return target[propKey]
})
}
那麼我們如何識别負指數呢?很容易出錯,是以我将更詳細地介紹。
首先,Proxy的get方法會攔截對數組所有屬性的通路,包括對數組索引的通路和對數組其他屬性的通路。僅當屬性名稱可以轉換為整數時,才會執行通路數組中元素的操作。我們實際上需要攔截這個操作來通路數組中的元素。
我們可以通過檢查是否可以将其轉換為整數來确定數組的屬性是否為索引。
Number(propKey) != NaN && Number.isInteger(Number(propKey))
是以,完整的代碼可以這樣寫:
function negativeArray(array) {
return new Proxy(array, {
get: function(target, propKey){
if (Number(propKey) != NaN && Number.isInteger(Number(propKey)) && Number(propKey) < 0) {
propKey = String(target.length + Number(propKey));
}
return target[propKey]
}
})
}
這是一個例子:
2、資料驗證
衆所周知,javascript 是一種弱類型語言。通常,建立對象時,它會裸運作。任何人都可以修改它。
但大多數時候,對象的屬性值需要滿足某些條件。例如,一個記錄使用者資訊的對象,其age字段中應該有一個大于0的整數,通常小于150。
let person1 = {
name: 'Jon',
age: 23
}
但是,預設情況下,JavaScript 不提供安全機制,我們可以随意更改此值。
person1.age = 9999
person1.age = 'hello world'
為了讓我們的代碼更安全,我們可以用 Proxy 包裝我們的對象。我們可以截取對象的set操作,驗證age字段的新值是否符合規則。
let ageValidate = {
set (item, property, value) {
if (property === 'age') {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new TypeError('age should be an integer between 0 and 150');
}
}
item[property] = value
}
}
現在,我們嘗試修改這個屬性的值,可以看到我們設定的保護機制在起作用。
3、關聯屬性
很多時候,一個對象的屬性是互相關聯的。例如,對于存儲使用者資訊的對象,其郵政編碼和位置是兩個高度相關的屬性,當使用者的郵政編碼确定後,他的位置也随之确定。
為了适應來自不同國家的讀者,我在這裡使用了一個虛拟示例。假設位置和郵編有如下關系:
JavaScript Street -- 232200
Python Street -- 234422
Golang Street -- 231142
這是用代碼表達它們的關系的結果。
const location2postcode = {
'JavaScript Street': 232200,
'Python Street': 234422,
'Golang Street': 231142
}
const postcode2location = {
'232200': 'JavaScript Street',
'234422': 'Python Street',
'231142': 'Golang Street'
}
然後看一個例子:
let person = {
name: 'Jon'
}
person.postcode = 232200
當我們設定 person.postcode=232200 時,我們希望能夠自動觸發 person.location='JavaScript Street'。
這是解決方案:
let postcodeValidate = {
set(item, property, value) {
if(property === 'location') {
item.postcode = location2postcode[value]
}
if(property === 'postcode'){
item.location = postcode2location[value]
}
}
}
是以,我們将postcode和location綁定在一起。
4、私有屬性
我們知道 JavaScript 從來不支援私有屬性。這使得我們在編寫代碼時無法合理地管理通路權限。
為了解決這個問題,JavaScript 社群的約定是以字元 _ 開頭的字段被視為私有屬性。
var obj = {
a: 1,
_value: 22
}
上面的 _value 屬性被認為是私有的。然而,重要的是要注意,這隻是一個約定,在語言層面沒有這樣的規則。
現在我們有了Proxy,我們可以模拟私有屬性特性。
與普通屬性相比,私有屬性具有以下特點:
- 無法讀取此屬性的值
- 當使用者嘗試通路對象的鍵時,該屬性不明顯
然後,我們可以檢視前面提到的Proxy的13個攔截操作,看到有3個操作需要攔截。
function setPrivateField(obj, prefix = "_"){
return new Proxy(obj, {
// Intercept the operation of `propKey in objProxy`
has: (obj, prop) => {},
// Intercept the operations such as `Object.keys(proxy)`
ownKeys: obj => {},
//Intercepts the reading operation of object properties
get: (obj, prop, rec) => {})
});
}
然後,我們在模闆中添加适當的判斷語句:如果發現使用者試圖通路以_開頭的字段,則拒絕通路。
function setPrivateField(obj, prefix = "_"){
return new Proxy(obj, {
has: (obj, prop) => {
if(typeof prop === "string" && prop.startsWith(prefix)){
return false
}
return prop in obj
},
ownKeys: obj => {
return Reflect.ownKeys(obj).filter(
prop => typeof prop !== "string" || !prop.startsWith(prefix)
)
},
get: (obj, prop) => {
if(typeof prop === "string" && prop.startsWith(prefix)){
return undefined
}
return obj[prop]
}
});
}
這是一個例子:
總結
以上就是我與你分享的關于Proxy的實用知識,如果你也覺得它對你有用的話,請記得點贊我,關注我,并将它分享給你身邊做開發的朋友,也許能夠幫助到他。
最後,感謝你的閱讀,祝程式設計愉快!