摘要: Proxy的騷操做。javascript
Fundebug經受權轉載,版權歸原做者全部。html
使用Proxy
,你能夠將一隻貓假裝成一隻老虎。下面大約有6個例子,我但願它們能讓你相信,Proxy 提供了強大的 Javascript 元編程。前端
儘管它不像其餘ES6功能用的廣泛,但Proxy
有許多用途,包括運算符重載,對象模擬,簡潔而靈活的API建立,對象變化事件,甚至Vue 3背後的內部響應系統提供動力。vue
Proxy
用於修改某些操做的默認行爲,也能夠理解爲在目標對象以前架設一層攔截,外部全部的訪問都必須先經過這層攔截,所以提供了一種機制,能夠對外部的訪問進行過濾和修改。這個詞的原理爲代理,在這裏能夠表示由它來「代理」某些操做,譯爲「代理器」。java
ES6原生提供了Proxy
構造函數,用來生成Proxy
實例。node
var proxy = new Proxy(target, handler);
Proxy
對象的全部用法,都是上面的這種形式。不一樣的只是handle
參數的寫法。其中new Proxy
用來生成Proxy
實例,target
是表示所要攔截的對象,handle
是用來定製攔截行爲的對象。git
下面是 Proxy 最簡單的例子是,這是一個有陷阱的代理,一個get
陷阱,老是返回42
。es6
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')
等。github
可是,Proxy 陷阱固然不限於屬性的讀取。 它只是十幾個不一樣陷阱中的一個:golang
在 Go 語言中,有零值的概念,零值是特定於類型的隱式默認結構值。其思想是提供類型安全的默認基元值,或者用gopher的話說,給結構一個有用的零值。
雖然不一樣的建立模式支持相似的功能,但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秒後沒法訪問。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
這些例子來自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。
Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對一、微脈、青團社等衆多品牌企業。歡迎你們免費試用!