阿里雲最近在作活動,低至2折,有興趣能夠看看:
https://promotion.aliyun.com/...
爲了保證的可讀性,本文采用意譯而非直譯。javascript
使用Proxy
,你能夠將一隻貓假裝成一隻老虎。下面大約有6個例子,我但願它們能讓你相信,Proxy 提供了強大的 Javascript 元編程。html
儘管它不像其餘ES6功能用的廣泛,但Proxy
有許多用途,包括運算符重載,對象模擬,簡潔而靈活的API建立,對象變化事件,甚至Vue 3背後的內部響應系統提供動力。前端
Proxy
用於修改某些操做的默認行爲,也能夠理解爲在目標對象以前架設一層攔截,外部全部的訪問都必須先經過這層攔截,所以提供了一種機制,能夠對外部的訪問進行過濾和修改。這個詞的原理爲代理,在這裏能夠表示由它來「代理」某些操做,譯爲「代理器」。vue
ES6原生提供了Proxy
構造函數,用來生成Proxy
實例。java
var proxy = new Proxy(target, handler);
Proxy
對象的全部用法,都是上面的這種形式。不一樣的只是handle
參數的寫法。其中new Proxy
用來生成Proxy
實例,target
是表示所要攔截的對象,handle
是用來定製攔截行爲的對象。node
下面是 Proxy 最簡單的例子是,這是一個有陷阱的代理,一個get
陷阱,老是返回42
。git
let target = { x: 10, y: 20 } let hanler = { get: (obj, prop) => 42 } target = new Proxy(target, hanler) target.x //42 target.y //42 target.x // 42
結果是一個對象將爲任何屬性訪問操做都返回「42」。 這包括target.x
,target['x']
,Reflect.get(target, 'x')
等。es6
可是,Proxy 陷阱固然不限於屬性的讀取。 它只是十幾個不一樣陷阱中的一個:github
在 Go 語言中,有零值的概念,零值是特定於類型的隱式默認結構值。其思想是提供類型安全的默認基元值,或者用gopher的話說,給結構一個有用的零值。golang
雖然不一樣的建立模式支持相似的功能,但Javascript沒法用隱式初始值包裝對象。Javascript中未設置屬性的默認值是undefined
。但 Proxy 能夠改變這種狀況。
const withZeroValue = (target, zeroValue) => new Proxy(target, { get: (obj, prop) => (prop in obj) ? obj[prop] : zeroValue })
函數withZeroValue
用來包裝目標對象。 若是設置了屬性,則返回屬性值。 不然,它返回一個默認的「零值」。
從技術上講,這種方法也不是隱含的,但若是咱們擴展withZeroValue
,以Boolean (false
), Number (0
), String (""
), Object ({}
),Array ([]
)等對應的零值,則多是隱含的。
let pos = { x: 4, y: 19 } console.log(pos.x, pos.y, pos.z) // 4, 19, undefined pos = withZeroValue(pos, 0) console.log(pos.z, pos.y, pos.z) // 4, 19, 0
此功能可能有用的一個地方是座標系。 繪圖庫能夠基於數據的形狀自動支持2D和3D渲染。 不是建立兩個單獨的模型,而是始終將z
默認爲 0
而不是undefined
,這多是有意義的。
在JS中獲取數組中的最後一個元素方式經過寫的很冗長且重複,也容易出錯。 這就是爲何有一個TC39提案定義了一個便利屬性Array.lastItem
來獲取和設置最後一個元素。
其餘語言,如Python和Ruby,使用負組索引更容易訪問最後面的元素。例如,能夠簡單地使用arr[-1]
替代arr[arr.length-1]
訪問最後一個元素。
使用 Proxy 也能夠在 Javascript 中使用負索引。
const negativeArray = (els) => new Proxy(els, { get: (target, propKey, receiver) => Reflect.get(target, (+propKey < 0) ? String(target.length + +propKey) : propKey, receiver) });
一個重要的注意事項是包含handler.get的陷阱字符串化全部屬性。 對於數組訪問,咱們須要將屬性名稱強制轉換爲Numbers
,這樣就可使用一元加運算符簡潔地完成。
如今[-1]
訪問最後一個元素,[-2]
訪問倒數第二個元素,以此類推。
const unicorn = negativeArray(['🐴', '🎂', '🌈']); unicorn[-1] // '🌈'
衆所周知 JS 沒有私有屬性。 Symbol
最初是爲了啓用私有屬性而引入的,但後來使用像Object.getOwnPropertySymbols
這樣的反射方法進行了淡化,這使得它們能夠被公開發現。
長期以來的慣例是將私有屬性命名爲前下劃線_
,有效地標記它們「不要訪問」。Prox
提供了一種稍微更好的方法來屏蔽這些屬性。
const hide = (target, prefix = '_') => new Proxy(target, { has: (obj, prop) => (!prop.startsWith(prefix) && prop in obj), ownKeys: (obj) => Reflect.ownKeys(obj) .filter(prop => (typeof prop !== "string" || !prop.startsWith(prefix))), get: (obj, prop, rec) => (prop in rec) ? obj[prop] : undefined })
hide
函數包裝目標對象,並使得從in
運算符和Object.getOwnPropertyNames
等方法沒法訪問帶有下劃線的屬性。
let userData = hide({ firstName: 'Tom', mediumHandle: '@tbarrasso', _favoriteRapper: 'Drake' }) userData._favoriteRapper // undefined ('_favoriteRapper' in userData) // false
更完整的實現還包括諸如deleteProperty
和defineProperty
之類的陷阱。 除了閉包以外,這多是最接近真正私有屬性的方法,由於它們沒法經過枚舉,克隆,訪問或修改來訪問。
可是,它們在開發控制檯中可見。 只有閉包才能免於這種命運。
在客戶端和服務器之間同步狀態時遇到困難並不罕見。數據可能會隨着時間的推移而發生變化,很難確切地知道什麼時候從新同步的邏輯。
Proxy
啓用了一種新方法:根據須要將對象包裝爲無效(和從新同步)屬性。 全部訪問屬性的嘗試都首先檢查緩存策略,該策略決定返回當前在內存中的內容仍是採起其餘一些操做。
const ephemeral = (target, ttl = 60) => { const CREATED_AT = Date.now() const isExpired = () => (Date.now() - CREATED_AT) > (ttl * 1000) return new Proxy(target, { get: (obj, prop) => isExpired() ? undefined : Reflect.get(obj, prop) }) }
這個函數過於簡化了:它使對象上的全部屬性在一段時間後都沒法訪問。然而,將此方法擴展爲根據每一個屬性設置生存時間(TTL),並在必定的持續時間或訪問次數以後更新它並不困難。
let bankAccount = ephemeral({ balance: 14.93 }, 10) console.log(bankAccount.balance) // 14.93 setTimeout(() => { console.log(bankAccount.balance) // undefined }, 10 * 1000)
這個示例簡單地使銀行賬戶餘額在10秒後沒法訪問。
這些例子來自Csaba Hellinge 關於[代理用例][23]和[Mozilla黑客][24]的文章。方法是包裝一個對象以防止擴展或修改。雖然
object.freeze`如今提供了將對象渲染爲只讀的功能,可是能夠對這種方法進行擴展,以便訪問不存在屬性的枚舉對象能更好地處理拋出錯誤。
const NOPE = () => { throw new Error("Can't modify read-only view"); } const NOPE_HANDLER = { set: NOPE, defineProperty: NOPE, deleteProperty: NOPE, preventExtensions: NOPE, setPrototypeOf: NOPE } const readOnlyView = target => new Proxy(target, NOPE_HANDLER)
const createEnum = (target) => readOnlyView(new Proxy(target, { get: (obj, prop) => { if (prop in obj) { return Reflect.get(obj, prop) } throw new ReferenceError(`Unknown prop "${prop}"`) } }))
如今咱們能夠建立一個Object
,若是嘗試訪問不存在的屬性如今不是返回undefined
,而是會拋出異常。 這使得在早期捕獲和解決問題變得更加容易。
咱們的enum
示例也是代理上的代理的第一個示例,它確認代理是另外一個代理的有效目標對象。這經過組合代理功能促進了代碼重用。
let SHIRT_SIZES = createEnum({ S: 10, M: 15, L: 20 }) SHIRT_SIZES.S // 10 SHIRT_SIZES.S = 15 // Uncaught Error: Can't modify read-only view SHIRT_SIZES.XL // Uncaught ReferenceError: Unknown prop "XL"
這種方法能夠進一步擴展,包括模擬方法nameOf
,它返回給定enum
值的屬性名,模仿Javascript等語言中的行爲。
雖然其餘框架和語言超集(好比TypeScript)提供enum
類型,可是這個解決方案的獨特之處在於,它使用普通Javascript,而不使用特殊的構建工具或轉置器。
也許從語法上講,最吸引人的 Proxy
用例是重載操做符的能力,好比使用handler.has的in操做符。
in
操做符用於檢查指定的屬性是否位於指定的對象或其原型鏈中。但它也是語法上最優雅的重載操做符。這個例子定義了一個連續range
函數來比較數字。
const range = (min, max) => new Proxy(Object.create(null), { has: (_, prop) => (+prop >= min && +prop <= max) })
與Python不一樣,Python使用生成器與有限的整數序列進行比較,這種方法支持十進制比較,能夠擴展爲支持其餘數值範圍。
const X = 10.5 const nums = [1, 5, X, 50, 100] if (X in range(1, 100)) { // true // ... } nums.filter(n => n in range(1, 10)) // [1, 5]
儘管這個用例不能解決複雜的問題,但它確實提供了乾淨、可讀和可重用的代碼。
除了in
運算符,咱們還能夠重載delete
和new
。
若是你曾經與cookie
進行交互,那麼必須處理document.cookie。 這是一個不尋常的API,由於API是一個String
,它讀出全部cookie
,以分號分隔。
document.cookie
是一個看起來像這樣的字符串:
_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1
簡而言之,處理document.cookie
比較麻煩且容易出錯。 一種方法是使用簡單的cookie框架,能夠適用於使用 Proxy。
const getCookieObject = () => { const cookies = document.cookie.split(';').reduce((cks, ck) => ({[ck.substr(0, ck.indexOf('=')).trim()]: ck.substr(ck.indexOf('=') + 1), ...cks}), {}); const setCookie = (name, val) => document.cookie = `${name}=${val}`; const deleteCookie = (name) => document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; return new Proxy(cookies, { set: (obj, prop, val) => (setCookie(prop, val), Reflect.set(obj, prop, val)), deleteProperty: (obj, prop) => (deleteCookie(prop), Reflect.deleteProperty(obj, prop)) }) }
此函數返回一個鍵值對對象,但代理對document.cookie
進行持久性的全部更改。
let docCookies = getCookieObject() docCookies.has_recent_activity // "1" docCookies.has_recent_activity = "2" // "2" delete docCookies2["has_recent_activity"] // true
在11行代碼中,修改cookie
提供了更好的交互,儘管在生產環境中還須要諸如字符串規範化之類的附加功能。
細節決定成敗,Proxy 也不例外。
在撰寫本文時(2019年5月),Proxy 沒有完整的 polyfill。然而,有一個由谷歌編寫的 partial polyfill for Proxy ,它支持get
、set
、apply
和construct trap
,並適用於IE9+。
肯定一個對象是不是代理是不可能的
根據Javascript語言規範,沒法肯定對象是不是代理。 可是,在 Node 10+上,可使用util.types.isProxy方法。
給定一個代理對象,就不可能得到或更改目標對象。也不可能獲取或修改處理程序對象。
最近似的是Ben Nadel的文章Using Proxy to Dynamically Change THIS Binding,它使用一個空對象做爲Proxy
目標和閉包來巧妙地從新分配對象的Proxy
操做。
new Proxy("To be, or not to be...", { }) // TypeError: Cannot create proxy with a non-object as target or handler
不幸的是,Proxy的一個限制是目標必須是Object。 這意味着咱們不能直接使用像String這樣的原語。 😞
Proxy的一個主要缺點是性能。 因瀏覽器和使用而異,可是對於性能有要求的代碼來講,代理不是最好的方法。 固然,能夠衡量影響並肯定代理的優點是否超過對性能的影響。
Proxy 提供虛擬化接口來控制任何目標 Object的行爲。 這樣作能夠在簡單性和實用性之間取得平衡,而不會犧牲兼容性。
也許使用Proxy
的最使人信服的理由是,上面的許多示例只有幾行,而且能夠輕鬆組合以建立複雜的功能。 最後一個例子,咱們能夠從幾個用例中組合函數來建立一個只讀cookie
對象,該對象返回不存在或「私有」隱藏cookie的默認值。
// document.cookie = "_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1" let docCookies = withZeroValue(hide(readOnlyView(getCookieObject())), "Cookie not found") docCookies.has_recent_activity // "1" docCookies.nonExistentCookie // "Cookie not found" docCookies._ga // "Cookie not found" docCookies.newCookie = "1" // Uncaught Error: Can't modify read-only view
我但願這些例子已經代表,對於Javascript中的小衆元編程來講,代理不只僅是一個深奧的特性。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
https://github.com/qq44924588...
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。