您是否曾經遇到過希望能夠控制對像或數組中值的情況?也許您想阻止某些類型的數據,甚至在將數據存儲到對像中之前驗證數據。假設您想以某種方式對傳入數據甚至傳出數據做出反應?例如,也許您想通過顯示結果來更新DOM 或交換樣式更改的類,因為數據會發生變化。是否曾經想過在一個只需要Vue 或React 等框架的一些功能的簡單頁面創意或部分上工作,但又不想啟動一個新應用程序?
那麼JavaScript Proxy 可能正是您所需要的!
我先聲明一下:在前端技術方面,我更像是一個UI 開發人員;就像描述的非JavaScript 集中型方面一樣,屬於“巨大分歧”的一部分。我很樂意只創建在瀏覽器中一致且具有所有相關特性的美觀的項目。因此,在更純粹的JavaScript 功能方面,我傾向於不深入研究。
然而,我仍然喜歡做研究,我總是在尋找一些東西來添加到我的新學習清單中。事實證明,JavaScript 代理是一個有趣的話題,因為僅僅回顧基礎知識就會打開許多關於如何利用此功能的可能性。儘管如此,乍一看,代碼可能會很快變得很重。當然,這完全取決於您的需求。
代理對象的概念已經存在了相當一段時間。在我的研究中,我可以找到幾年前的參考資料。然而,它在我的清單上並不靠前,因為它從未在Internet Explorer 中得到支持。相比之下,多年來,它在所有其他瀏覽器中都得到了極好的支持。這就是Vue 3 與Internet Explorer 11 不兼容的原因之一,因為在最新的Vue 項目中使用了代理。
那麼,代理對象究竟是什麼呢?
MDN 將Proxy 對象描述為:
[…] 使您能夠為另一個對象創建一個代理,該代理可以攔截並重新定義該對象的根本操作。
總的想法是,您可以創建一個具有功能的對象,讓您控制使用對象時發生的典型操作。最常見的兩個是獲取和設置存儲在對像中的值。
const myObj = { mykey: 'value' } console.log(myObj.mykey); // "獲取" 密鑰的值,輸出'value' myObj.mykey = 'updated'; // "設置" 密鑰的值,使其變為'updated'
因此,在我們的代理對像中,我們將創建“陷阱”來攔截這些操作並執行我們可能希望完成的任何功能。最多可以有十三種這樣的陷阱可用。我不一定會涵蓋所有這些陷阱,因為並非所有這些陷阱對於我下面提供的簡單示例都是必要的。再次聲明,這取決於您為創建內容的特定上下文所需的內容。相信我,僅僅掌握基礎知識就可以走很遠。
為了擴展上面的示例以創建代理,我們將執行以下操作:
const myObj = { mykey: 'value' } const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } const proxy = new Proxy(myObj, handler); console.log(proxy.mykey); // "獲取" 密鑰的值,輸出'value' proxy.mykey = 'updated'; // "設置" 密鑰的值,使其變為'updated'
首先,我們從標準對像開始。然後,我們創建一個處理程序對象,該對象保存處理程序函數,通常稱為陷阱。這些表示可以對傳統對象執行的操作,在本例中,這些操作只是在沒有任何更改的情況下傳遞內容。之後,我們使用帶有目標對象和處理程序對象的構造函數創建我們的代理。那時,我們可以引用代理對象來獲取和設置值,這些值將是原始目標對象myObj 的代理。
請注意set 陷阱末尾的return true。這旨在通知代理設置值應被視為成功。在某些情況下,如果您希望阻止設置值(考慮驗證錯誤),則應返回false。這還會導致控制台錯誤,並輸出TypeError。
現在,記住這種模式的一件事是原始目標對象仍然可用。這意味著您可以繞過代理並更改對象的值,而無需使用代理。在我閱讀有關使用Proxy 對象的內容時,我發現了一些有用的模式可以幫助解決這個問題。
let myObj = { mykey: 'value' } const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } myObj = new Proxy(myObj, handler); console.log(myObj.mykey); // "獲取" 密鑰的值,輸出'value' myObj.mykey = 'updated'; // "設置" 密鑰的值,使其變為'updated'
在這種模式下,我們使用目標對像作為代理對象,同時在代理構造函數中引用目標對象。是的,就是這樣。這有效,但我發現很容易混淆正在發生的事情。因此,讓我們在代理構造函數中創建目標對象:
const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } const proxy = new Proxy({ mykey: 'value' }, handler); console.log(proxy.mykey); // "獲取" 密鑰的值,輸出'value' proxy.mykey = 'updated'; // "設置" 密鑰的值,使其變為'updated'
事實上,如果我們願意,我們可以在構造函數中創建目標對象和處理程序對象:
const proxy = new Proxy({ mykey: 'value' }, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } }); console.log(proxy.mykey); // "獲取" 密鑰的值,輸出'value' proxy.mykey = 'updated'; // "設置" 密鑰的值,使其變為'updated'
事實上,這是我在下面的示例中最常用的模式。值得慶幸的是,創建代理對象的方式很靈活。只需使用適合您的任何模式即可。
以下是一些示例,涵蓋了從基本數據驗證到使用fetch 更新表單數據的JavaScript Proxy 的用法。請記住,這些示例確實涵蓋了JavaScript Proxy 的基礎知識;如果您願意,它可以很快深入研究。在某些情況下,它們只是在代理對像中創建常規JavaScript 代碼來執行常規JavaScript 操作。將它們視為通過更多地控制數據來擴展一些常見JavaScript 任務的方法。
我的第一個示例涵蓋了我一直覺得是一個相當簡單而奇怪的編碼面試問題:反轉字符串。我一直不喜歡這個問題,在進行面試時也從不問這個問題。作為一個喜歡在這種事情上逆流而上的人,我嘗試了非傳統的解決方案。你知道,只是為了好玩有時會把它拋出來,其中一個解決方案是很棒的前端樂趣。它也提供了一個簡單的示例,展示了代理的用法。
如果您在輸入中鍵入內容,您將看到鍵入的內容在下方以反向方式打印出來。顯然,可以使用許多反轉字符串的方法。然而,讓我們來看看我這種奇怪的反轉方法。
const reverse = new Proxy( { value: '' }, { set: function (target, prop, value) { target[prop] = value; document.querySelectorAll('[data-reverse]').forEach(item => { let el = document.createElement('div'); el.innerHTML = '\u{202E}' value; item.innerText = el.innerHTML; }); return true; } } ) document.querySelector('input').addEventListener('input', e => { reverse.value = e.target.value; });
首先,我們創建新的代理,目標對像是一個單鍵值,它保存輸入中鍵入的任何內容。 get 陷阱不存在,因為我們只需要簡單的直通,因為我們沒有任何實際的功能與之綁定。在這種情況下,無需執行任何操作。我們稍後會討論這個問題。
對於set 陷阱,我們確實有一些功能需要執行。仍然有一個簡單的直通,其中值像往常一樣設置為目標對像中的value 密鑰。然後有一個querySelectorAll 查找頁面上所有具有data-reverse 數據屬性的元素。這允許我們一次性定位頁面上的多個元素並更新它們。這為我們提供了每個人都喜歡看到的框架式綁定操作。這也可以更新為定位輸入,以允許適當的雙向綁定類型的情況。
這就是我反轉字符串的古怪方法發揮作用的地方。一個div 在內存中創建,然後使用字符串更新元素的innerHTML。字符串的第一部分使用特殊的Unicode 十進制代碼,實際上會反轉後面的所有內容,使其從右到左。然後,頁面上實際元素的innerText 將獲得內存中div 的innerHTML。每次在輸入中輸入內容時都會運行此操作;因此,所有具有data-reverse 屬性的元素都會更新。
最後,我們在輸入上設置一個事件偵聽器,該偵聽器通過輸入的值(即事件的目標)設置目標對像中的value 密鑰。
最後,一個非常簡單的示例,通過將值設置為對象來對頁面的DOM 執行副作用。
常見的UI 模式是將輸入的值格式化為比僅僅是字母和數字字符串更精確的序列。這方面的一個例子是電話輸入。有時,如果鍵入的電話號碼看起來像電話號碼,它看起來和感覺更好。不過,訣竅是,當我們格式化輸入的值時,我們可能仍然需要數據的非格式化版本。
對於JavaScript Proxy 來說,這是一項簡單的任務。
當您在輸入中鍵入數字時,它們將被格式化為標準的美國電話號碼(例如(123) 456-7890)。請注意,電話號碼也以純文本形式顯示在輸入下方,就像上面的反向字符串示例一樣。該按鈕將數據的格式化版本和非格式化版本都輸出到控制台。
因此,以下是代理的代碼:
const phone = new Proxy( { _clean: '', number: '', get clean() { return this._clean; } }, { get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' } }, set: function (target, prop, value) { if (!prop.startsWith('_')) { target._clean = value.replace(/\D/g, '').substring(0, 10); const sections = { area: target._clean.substring(0, 3), prefix: target._clean.substring(3, 6), line: target._clean.substring(6, 10) } target.number = target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}` : target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}` : target._clean.length > 0 ? `(${sections.area}` : ''; document.querySelectorAll('[data-phone_number]').forEach(item => { if (item.tagName === 'INPUT') { item.value = target.number; } else { item.innerText = target.number; } }); return true; } else { return false; } } } );
此示例中的代碼更多,因此讓我們將其分解。第一部分是我們正在代理內部初始化的目標對象。它有三個方面。
{ _clean: '', number: '', get clean() { return this._clean; } },
第一個鍵_clean 是我們的變量,它保存數據的非格式化版本。它以下劃線開頭,採用傳統的變量命名模式,將其視為“私有”。我們希望在正常情況下使其不可用。隨著我們的深入,我們將對此進行更多介紹。
第二個鍵number 簡單地保存格式化的電話號碼值。
第三個“鍵”是使用名稱clean 的get 函數。這將返回我們私有_clean 變量的值。在這種情況下,我們只是返回該值,但如果我們願意,這提供了對它執行其他操作的機會。這就像代理的get 函數的代理getter。這似乎很奇怪,但它為控制數據提供了一種簡單的方法。根據您的具體需求,這可能是一種處理這種情況的相當簡單的方法。它適用於我們這裡的簡單示例,但可能需要採取其他步驟。
現在是代理的get 陷阱。
get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' } },
首先,我們檢查傳入的prop 或對象鍵,以確定它是否不以下劃線開頭。如果它不以下劃線開頭,我們只需返回它。如果它以下劃線開頭,那麼我們返回一個字符串,說明未找到條目。根據需要,可以以不同的方式處理這種類型的負面返回。返回字符串、返回錯誤或運行具有不同副作用的代碼。這完全取決於具體情況。
在我的示例中需要注意的一點是,我沒有處理可能與在代理中被認為是私有變量的內容一起使用的其他代理陷阱。為了更全面地保護這些數據,您必須考慮其他陷阱,例如[defineProperty]( https://www.php.cn/link/cd69510f4a69bc0ef6ba504331b9d546或ownKeys——通常是關於操作或引用對象鍵的任何內容。您是否走這麼遠可能取決於誰將使用代理。如果它用於您自己,那麼您就知道如何使用代理。但如果它是其他人,您可能需要考慮盡可能多地鎖定內容。
現在是此示例中大部分魔法發生的地方——set 陷阱:
set: function (target, prop, value) { if (!prop.startsWith('_')) { target._clean = value.replace(/\D/g, '').substring(0, 10); const sections = { area: target._clean.substring(0, 3), prefix: target._clean.substring(3, 6), line: target._clean.substring(6, 10) } target.number = target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}` : target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}` : target._clean.length > 0 ? `(${sections.area}` : ''; document.querySelectorAll('[data-phone_number]').forEach(item => { if (item.tagName === 'INPUT') { item.value = target.number; } else { item.innerText = target.number; } }); return true; } else { return false; } }
首先,對代理中私有變量進行相同的檢查。我並沒有真正測試其他類型的prop,但您可能需要在這裡考慮這樣做。我假設只有代理目標對像中的number 鍵將被調整。
傳入的值(輸入的值)將剝離除數字字符以外的所有內容,並保存到_clean 鍵。然後在整個過程中使用此值來重建為格式化的值。基本上,每次鍵入時,整個字符串都會實時重建為預期的格式。 substring 方法將數字鎖定為十位數。
然後創建一個sections 對象來保存我們電話號碼的不同部分,這些部分基於美國電話號碼的細分。隨著_clean 變量長度的增加,我們將number 更新為我們希望在那時看到的格式模式。
一個querySelectorAll 正在查找任何具有data-phone_number 數據屬性的元素,並通過forEach 循環運行它們。如果該元素是更新值的輸入,則更新其他任何元素的innerText。這就是文本如何在輸入下方顯示的方式。如果我們要放置另一個具有該數據屬性的輸入元素,我們將看到它的值實時更新。這是一種創建單向或雙向綁定的方法,具體取決於需求。
最後,返回true 以讓代理知道一切順利。如果傳入的prop 或鍵以下劃線開頭,則返回false。
最後,使這能夠工作的事件偵聽器:
document.querySelectorAll('input[data-phone_number]').forEach(item => { item.addEventListener('input', (e) => { phone.number = e.target.value; }); }); document.querySelector('#get_data').addEventListener('click', (e) => { console.log(phone.number); // (123) 456-7890 console.log(phone.clean); // 1234567890 });
第一組查找所有具有我們特定數據屬性的輸入,並向它們添加事件偵聽器。對於每個輸入事件,代理的number 鍵值都將使用當前輸入的值進行更新。由於我們每次發送輸入的值時都會對其進行格式化,因此我們刪除了任何不是數字的字符。
第二組查找輸出兩組數據的按鈕(按要求),輸出到控制台。這顯示了我們如何編寫代碼來按需請求所需的數據。希望很清楚的是,phone.clean 正在引用目標對像中的get 代理函數,該函數返回對像中的_clean 變量。請注意,它沒有像phone.clean() 一樣被調用為函數,因為它在我們的代理中充當get 代理。
您可以使用數組作為代理中的目標“對象”而不是對象。由於它將是一個數組,因此需要考慮一些事項。數組的功能(例如push())將在代理的setter 陷阱中以某種方式處理。此外,在這種情況下,在目標對象概念中創建自定義函數實際上不起作用。然而,將數組作為目標可以做一些有用的事情。
當然,在數組中存儲數字並不是什麼新鮮事。顯然。然而,我將向這個數字存儲數組附加一些規則,例如不允許重複值並且只允許數字。我還將提供一些輸出選項,例如排序、求和、平均值和清除值。然後更新一個控制所有這些的小型用戶界面。
以下是代理對象:
const numbers = new Proxy([], { get: function (target, prop) { message.classList.remove('error'); if (prop === 'sort') return [...target].sort((a, b) => a - b); if (prop === 'sum') return [...target].reduce((a, b) => ab); if (prop === 'average') return [...target].reduce((a, b) => ab) / target.length; if (prop === 'clear') { message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`; target.splice(0, target.length); collection.innerText = target; } return target[prop]; }, set: function (target, prop, value) { if (prop === 'length') return true; dataInput.value = ''; message.classList.remove('error'); if (!Number.isInteger(value)) { console.error('Data provided is not a number!'); message.innerText = 'Data provided is not a number!'; message.classList.add('error'); return false; } if (target.includes(value)) { console.error(`Number ${value} has already been submitted!`); message.innerText = `Number ${value} has already been submitted!`; message.classList.add('error'); return false; } target[prop] = value; collection.innerText = target; message.innerText = `Number ${value} added!`; return true; } });
對於此示例,我將從setter 陷阱開始。
首先要做的是檢查是否將length 屬性設置為數組。它只是返回true,以便它以通常的方式發生。如果需要對設置的長度做出反應,它始終可以在適當的位置添加代碼。
接下來的兩行代碼引用頁面上使用querySelector 存儲的兩個HTML 元素。 dataInput 是輸入元素,我們希望在每次輸入時都清除它。 message 是保存對數組更改的響應的元素。由於它具有錯誤狀態的概念,因此我們確保它在每次輸入時都不處於該狀態。
第一個if 檢查輸入是否實際上是一個數字。如果不是,那麼它會執行幾件事。它發出一個控制台錯誤,說明問題所在。 message 元素獲得相同的語句。然後,message 通過CSS 類進入錯誤狀態。最後,它返回false,這也會導致代理向控制台發出它自己的錯誤。
第二個if 檢查輸入是否已存在於數組中;記住我們不希望重複。如果存在重複,則會發生與第一個if 中相同的消息傳遞。消息傳遞略有不同,因為它是一個模板文字,因此我們可以看到重複的值。
最後一部分假設一切順利,並且可以繼續進行。值像往常一樣設置,然後我們更新collection 列表。 collection 引用頁面上的另一個元素,該元素向我們顯示數組中當前的數字集合。同樣,消息會使用已添加的條目進行更新。最後,我們返回true 以讓代理知道一切順利。
現在,get 陷阱與之前的示例略有不同。
get: function (target, prop) { message.classList.remove('error'); if (prop === 'sort') return [...target].sort((a, b) => a - b); if (prop === 'sum') return [...target].reduce((a, b) => ab); if (prop === 'average') return [...target].reduce((a, b) => ab) / target.length; if (prop === 'clear') { message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`; target.splice(0, target.length); collection.innerText = target; } return target[prop]; },
這裡發生的事情是利用了一個不是普通數組方法的“prop”;它作為prop 傳遞給get 陷阱。例如,第一個“prop”由這個事件偵聽器觸發:
dataSort.addEventListener('click', () => { message.innerText = numbers.sort; });
因此,當單擊sort 按鈕時,message 元素的innerText 將使用numbers.sort 返回的內容進行更新。它充當代理攔截並返回非典型數組相關結果的getter。
刪除message 元素的潛在錯誤狀態後,我們然後確定是否預期會發生非標準數組獲取操作。每個操作都會返回對原始數組數據的操作,而不會更改原始數組。這是通過使用目標上的擴展運算符來創建一個新數組,然後使用標準數組方法來完成的。每個名稱都應該暗示它所做的操作:排序、求和、平均值和清除。好吧,清除並不是一個標準的數組方法,但這聽起來不錯。由於條目可以按任何順序排列,因此我們可以讓它為我們提供排序列表或對條目執行數學函數。清除簡單地清除數組,正如您可能預期的那樣。
以下是用於按鈕的其他事件偵聽器:
dataForm.addEventListener('submit', (e) => { e.preventDefault(); numbers.push(Number.parseInt(dataInput.value)); }); dataSubmit.addEventListener('click', () => { numbers.push(Number.parseInt(dataInput.value)); }); dataSort.addEventListener('click', () => { message.innerText = numbers.sort; }); dataSum.addEventListener('click', () => { message.innerText = numbers.sum; }); dataAverage.addEventListener('click', () => { message.innerText = numbers.average; }); dataClear.addEventListener('click', () => { numbers.clear; });
我們可以通過多種方式擴展和向數組添加功能。我見過允許使用負索引選擇條目的數組示例,該負索引從末尾計數。根據對象內的屬性值查找對像數組中的條目。嘗試在數組中獲取不存在的值時返回消息而不是undefined。有很多想法可以利用和探索數組上的代理。
地址表單在網頁上是相當標準的東西。讓我們為它添加一些交互性以獲得樂趣(和非標準)確認。它還可以充當表單值在單個對像中的數據收集,可以按需請求。
以下是代理對象:
const model = new Proxy( { name: '', address1: '', address2: '', city: '', state: '', zip: '', getData() { return { name: this.name || 'no entry!', address1: this.address1 || 'no entry!', address2: this.address2 || 'no entry!', city: this.city || 'no entry!', state: this.state || 'no entry!', zip: this.zip || 'no entry!' }; } }, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; if (prop === 'zip' && value.length === 5) { fetch(`https://api.zippopotam.us/us/${value}`) .then(response => response.json()) .then(data => { model.city = data.places[0]['place name']; document.querySelector('[data-model="city"]').value = target.city; model.state = data.places[0]['state abbreviation']; document.querySelector('[data-model="state"]').value = target.state; }); } document.querySelectorAll(`[data-model="${prop}"]`).forEach(item => { if (item.tagName === 'INPUT' || item.tagName === 'SELECT') { item.value = value; } else { item.innerText = value; } }) return true; } } );
目標對象非常簡單;表單中每個輸入的條目。 getData 函數將返回對象,但如果屬性的值為空字符串,它將更改為“no entry!”這是可選的,但該函數提供比我們僅通過獲取代理對象的狀態所能獲得的更清晰的對象。
getter 函數只是像往常一樣傳遞內容。您可能不需要這樣做,但我喜歡將其包含在內以使其完整。
setter 函數將值設置為prop。但是,if 檢查是否將要設置的prop 恰好是郵政編碼。如果是,那麼我們檢查值長度是否為5。當評估為true 時,我們將執行一個fetch,該fetch 使用郵政編碼命中地址查找器API。返回的任何值都將插入到對象屬性、城市輸入中,並在select 元素中選擇州。這是一個方便的快捷方式示例,可以讓用戶不必鍵入這些值。如果需要,可以手動更改這些值。
對於下一部分,讓我們來看一個輸入元素的示例:
<code></code>
代理有一個querySelectorAll,它查找任何具有匹配數據屬性的元素。這與我們之前看到的反向字符串示例相同。如果它找到匹配項,它將更新輸入的值或元素的innerText。這就是旋轉卡實時更新以顯示完成的地址外觀的方式。
需要注意的一點是輸入上的data-model 屬性。該數據屬性的值實際上會在其操作過程中告知代理要鎖定的鍵。代理根據涉及的鍵查找涉及的元素。事件偵聽器通過讓代理知道哪個鍵正在使用來執行相同的操作。這就是它的樣子:
document.querySelector('main').addEventListener('input', (e) => { model[e.target.dataset.model] = e.target.value; });
因此,將定位main 元素內的所有輸入,並且當觸發輸入事件時,代理將更新。 data-model 屬性的值用於確定要定位代理中的哪個鍵。實際上,我們正在使用模型系統。考慮一下這種事情如何進一步利用。
至於“獲取數據”按鈕?它是getData 函數的簡單控制台日誌……
getDataBtn.addEventListener('click', () => { console.log(model.getData()); });
這是一個有趣的示例,用於構建和使用以探索該概念。這就是讓我思考我可以使用JavaScript Proxy 構建什麼的示例。有時,您只需要一個小型窗口小部件,它具有一些數據收集/保護功能,並且只需與數據交互即可操作DOM。是的,您可以使用Vue 或React,但有時即使它們對於如此簡單的事情來說也可能過於復雜。
“就此為止”的意思是,這取決於你們每個人以及您是否會更深入地研究JavaScript Proxy。正如我在本文開頭所說,我只介紹了此功能的基礎知識。它可以提供更多功能,並且可以比我提供的示例更大。在某些情況下,它可以為利基解決方案提供小型幫助程序的基礎。很明顯,可以使用執行相同功能的基本函數輕鬆創建這些示例。甚至我的大部分示例代碼都是常規JavaScript 與代理對象混合在一起。
不過,重點是提供使用代理的示例,以展示如何對數據交互做出反應——甚至控制如何對這些交互做出反應以保護數據、驗證數據、操作DOM 和獲取新數據——所有這些都基於某人嘗試保存或獲取數據。從長遠來看,這可能非常強大,並且允許創建可能不需要更大庫或框架的簡單應用程序。
因此,如果您是一位更關注UI 方面的前端開發人員,就像我一樣,您可以探索一些基礎知識,看看是否有可能從JavaScript Proxy 中獲益的小型項目。如果您更是一位JavaScript 開發人員,那麼您可以開始更深入地研究代理以用於更大的項目。也許是一個新的框架或庫?
只是一個想法……
以上是JavaScript代理的介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!