4 個實(shí)用示例幫助你掌握 JavaScript 中Proxy功能

英文 | https://javascript.plainenglish.io/why-proxies-in-javascript-are-fantastic-db100ddc10a0
翻譯 | 楊小愛
什么是Proxy?它究竟是做什么的?在解釋這一點(diǎn)之前,讓我們看一個真實(shí)世界的開發(fā)例子。
我們每個人在日常生活中都有很多事情要做,比如看郵件、收快遞等等。有時我們可能會感到有點(diǎn)焦慮:我們的郵件列表上有很多垃圾郵件,需要花費(fèi)大量時間來篩選它們;收到的貨物可能含有恐怖分子安放的炸彈,威脅到我們的安全。
那么,這時你可能需要一個忠誠的管家,您希望管家?guī)椭鷪?zhí)行以下操作:在您開始閱讀之前讓其檢查您的收件箱并刪除所有垃圾郵件;當(dāng)您收到包裹時,請它用專業(yè)設(shè)備檢查包裹,確保里面沒有炸彈。
在上面的例子中,管家就是我們的代理,當(dāng)我們試圖做某事時,管家為我們做了一些額外的事情。
現(xiàn)在讓我們回到 JavaScript,我們知道 JavaScript 是一種面向?qū)ο蟮木幊陶Z言,沒有對象我們就無法編寫代碼。但是 JavaScript 對象總是裸奔的,你可以用它們做任何事情。很多時候,這會降低我們的代碼安全性。
所以在 ECMAScript2015 中引入了 Proxy 功能。有了Proxy,我們可以為物件找到忠實(shí)的管家,幫助我們增強(qiáng)物件原有的功能。
在最基本的層面上,使用 Proxy 的語法看起來像這樣:
// This is a normal objectlet obj = {a: 1, b:2}// Configure obj with a housekeeper using Proxylet objProxy = new Proxy(obj, handler)
這只是代碼的示例,因?yàn)槲覀冞€沒有寫handler,所以這段代碼暫時還不能正常運(yùn)行。
對于一個人來說,我們可能有閱讀郵件、取件等操作,管家可以為我們做這些。對于一個對象,我們可以讀取屬性、設(shè)置屬性等等,這些也可以通過代理對象來增強(qiáng)。
在處理程序中,我們可以列出我們想要代理的操作。例如,如果我們想在獲取對象屬性的同時在控制臺中打印出一條語句,我們可以這樣寫:
let obj = {a: 1, b:2}// Use Proxy syntax to find a housekeeper for the objectlet 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]}
當(dāng)我們嘗試讀取對象的屬性時,get 函數(shù)就會執(zhí)行。

get 函數(shù)可以接受三個參數(shù):
item :它是對象本身。
proerty :您要讀取的屬性的名稱。
itemProxy :它是我們剛剛創(chuàng)建的管家對象。
你可能已經(jīng)在其他地方閱讀過有關(guān) Proxy 的教程,并且您會注意到我對參數(shù)的命名與它們不同。我這樣做是為了更接近我之前的示例,以幫助你理解。我希望它對你有用。
那么get函數(shù)的返回值就是讀取這個屬性的結(jié)果。因?yàn)槲覀冞€不想改變?nèi)魏螙|西,所以我們只返回原始對象的屬性值。
如果需要,我們也可以更改結(jié)果。例如,我們可以這樣做:
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}})
以下是讀取它的屬性的結(jié)果:

我們將跟進(jìn)實(shí)際示例來說明此技巧的實(shí)際用途。
除了攔截對屬性的讀取,我們還可以攔截對屬性的修改。像這樣:
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}})
當(dāng)我們嘗試設(shè)置對象屬性的值時,會觸發(fā) set 函數(shù)。

因?yàn)槲覀冊谠O(shè)置屬性值時需要傳遞一個額外的值,所以上面的 set 函數(shù)比 get 函數(shù)多了一個參數(shù)。
除了攔截對屬性的讀取和修改外,Proxy 總共可以攔截對對象的 13 種操作。
他們是:
get(item, propKey, itemProxy):攔截對象屬性的讀取操作,如obj.a和ojb['b']
set(item, propKey, value, itemProxy):攔截對象屬性的設(shè)置操作,如 obj.a = 1 。
has(item, propKey):攔截objProxy中propKey的操作,返回一個布爾值。
deleteProperty(item, propKey):攔截delete proxy[propKey]的操作,返回一個布爾值。
ownKeys(item):攔截Object.getOwnPropertyNames(proxy),Object.getOwnPropertySymbols(proxy),Object.keys(proxy),for...in等操作,返回一個數(shù)組。該方法返回目標(biāo)對象自身所有屬性的屬性名,而 Object.keys() 的返回結(jié)果只包含目標(biāo)對象自身的可枚舉屬性。
getOwnPropertyDescriptor(item, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey)的操作,返回屬性的描述符。
defineProperty(item, propKey, propDesc):攔截這些操作:Object.defineProperty(proxy, propKey, propDesc),Object.defineProperties(proxy, propDescs),返回一個布爾值。
preventExtensions(item):攔截Object.preventExtensions(proxy)的操作,返回一個布爾值。
getPrototypeOf(item):攔截Object.getPrototypeOf(proxy)的操作,返回一個對象。
isExtensible(item):攔截Object.isExtensible(proxy)的操作,返回一個布爾值。
setPrototypeOf(item, proto):攔截Object.setPrototypeOf(proxy, proto)的操作,返回一個布爾值。
如果目標(biāo)對象是一個函數(shù),還有兩個額外的操作要intercept.s
apply(item, object, args):攔截函數(shù)調(diào)用操作,如proxy(...args),proxy.call(object, ...args),proxy.apply(...)。
constructor(item, args):攔截Proxy實(shí)例調(diào)用的操作作為構(gòu)造函數(shù),如new proxy(...args)。
有些攔截不常用,我就不細(xì)說了?,F(xiàn)在讓我們進(jìn)入現(xiàn)實(shí)世界的例子,看看 Proxy 可以為我們做什么。
1、實(shí)現(xiàn)數(shù)組的負(fù)索引
我們知道其他一些編程語言,例如 Python,支持對數(shù)組的負(fù)索引訪問。
負(fù)索引以數(shù)組的最后一個位置為起點(diǎn)并向前計(jì)數(shù)。如:
arr[-1] 是數(shù)組的最后一個元素。
arr[-3] 是數(shù)組中倒數(shù)第三個元素。
許多人認(rèn)為這是一個非常有用的功能,但不幸的是,JavaScript 目前不支持負(fù)索引語法。

但是 JavaScript 中強(qiáng)大的 Proxy 給了我們元編程的能力。
我們可以將數(shù)組包裝為 Proxy 對象。當(dāng)用戶試圖訪問一個負(fù)數(shù)索引時,我們可以通過 Proxy 的 get 方法攔截這個操作。然后根據(jù)之前定義的規(guī)則將負(fù)索引轉(zhuǎn)換為正索引,訪問就完成了。
讓我們從一個基本操作開始:攔截對數(shù)組屬性的讀取。
function negativeArray(array) {return new Proxy(array, {get: function(item, propKey){console.log(propKey)return item[propKey]}})}
上面的函數(shù)可以包裝一個數(shù)組,讓我們看看它是如何使用的。

如您所見,我們對數(shù)組屬性的讀取確實(shí)被攔截了。
請注意:JavaScript 中的對象只能有一個字符串或符號類型的鍵,當(dāng)我們寫 arr[1] 時,它實(shí)際上是在訪問 arr['1'] ,鍵是字符串“1”,而不是數(shù)字 1。
所以現(xiàn)在我們需要做的是:當(dāng)用戶試圖訪問一個屬性是數(shù)組的索引時,發(fā)現(xiàn)它是一個負(fù)索引,然后進(jìn)行相應(yīng)的攔截和處理;如果屬性不是索引,或者索引是正數(shù),我們什么也不做。
結(jié)合以上需求,我們可以編寫如下模板代碼。
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]})}
那么我們?nèi)绾巫R別負(fù)指數(shù)呢?很容易出錯,所以我將更詳細(xì)地介紹。
首先,Proxy的get方法會攔截對數(shù)組所有屬性的訪問,包括對數(shù)組索引的訪問和對數(shù)組其他屬性的訪問。僅當(dāng)屬性名稱可以轉(zhuǎn)換為整數(shù)時,才會執(zhí)行訪問數(shù)組中元素的操作。我們實(shí)際上需要攔截這個操作來訪問數(shù)組中的元素。
我們可以通過檢查是否可以將其轉(zhuǎn)換為整數(shù)來確定數(shù)組的屬性是否為索引。
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、數(shù)據(jù)驗(yàn)證
眾所周知,javascript 是一種弱類型語言。通常,創(chuàng)建對象時,它會裸運(yùn)行。任何人都可以修改它。
但大多數(shù)時候,對象的屬性值需要滿足某些條件。例如,一個記錄用戶信息的對象,其age字段中應(yīng)該有一個大于0的整數(shù),通常小于150。
let person1 = {name: 'Jon',age: 23}
但是,默認(rèn)情況下,JavaScript 不提供安全機(jī)制,我們可以隨意更改此值。
person1.age = 9999person1.age = 'hello world'
為了讓我們的代碼更安全,我們可以用 Proxy 包裝我們的對象。我們可以截取對象的set操作,驗(yàn)證age字段的新值是否符合規(guī)則。
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}}
現(xiàn)在,我們嘗試修改這個屬性的值,可以看到我們設(shè)置的保護(hù)機(jī)制在起作用。

很多時候,一個對象的屬性是相互關(guān)聯(lián)的。例如,對于存儲用戶信息的對象,其郵政編碼和位置是兩個高度相關(guān)的屬性,當(dāng)用戶的郵政編碼確定后,他的位置也隨之確定。
為了適應(yīng)來自不同國家的讀者,我在這里使用了一個虛擬示例。假設(shè)位置和郵編有如下關(guān)系:
JavaScript Street -- 232200Python Street -- 234422Golang Street -- 231142
這是用代碼表達(dá)它們的關(guān)系的結(jié)果。
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
當(dāng)我們設(shè)置 person.postcode=232200 時,我們希望能夠自動觸發(fā) 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 從來不支持私有屬性。這使得我們在編寫代碼時無法合理地管理訪問權(quán)限。
為了解決這個問題,JavaScript 社區(qū)的約定是以字符 _ 開頭的字段被視為私有屬性。
var obj = {a: 1,_value: 22}
上面的 _value 屬性被認(rèn)為是私有的。然而,重要的是要注意,這只是一個約定,在語言層面沒有這樣的規(guī)則。
現(xiàn)在我們有了Proxy,我們可以模擬私有屬性特性。
與普通屬性相比,私有屬性具有以下特點(diǎn):
無法讀取此屬性的值
當(dāng)用戶嘗試訪問對象的鍵時,該屬性不明顯
然后,我們可以查看前面提到的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 propertiesget: (obj, prop, rec) => {})});}
然后,我們在模板中添加適當(dāng)?shù)呐袛嗾Z句:如果發(fā)現(xiàn)用戶試圖訪問以_開頭的字段,則拒絕訪問。
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]}});}
這是一個例子:

學(xué)習(xí)更多技能
請點(diǎn)擊下方公眾號
![]()

