es6 proxy淺析
Proxy
使用proxy,你可以把老虎偽裝成貓的外表,這有幾個例子,希望能讓你感受到proxy的威力。
proxy 用來定義自定義的基本操作行為,比如查詢、賦值、列舉性、函式呼叫等。
proxy接受一個待代理目標物件和一些包含元操作的物件,為待代理目標建立一個‘屏障’,並攔截所有操作,重定向到自定義的元操作物件上。
proxy通過new Proxy
來建立,接受兩個引數:
- 待代理目標物件
- 元操作物件
閒話少說,直接看例子。
最簡單的只代理一個方功能,在這個例子裡,我們讓get
操作,永遠返回一個固定的值
let target = { name: 'fox', age: 23 } let handler = { get: (obj, k) => 233 } target = new Proxy(target, handler); target.a // 233 target.b // 233 target.c // 233
無論你taget.x
、target[x]
、Reflect.get(target, 'x')
都會返回233
當然,代理get
僅僅是其中一種操作,還有:
- get
- set
- has
- apply
- construct
- ownKeys
- deleteProperty
- defineProperty
- isExtensible
- preventExtensions
- getPrototypeOf
- setPrototypeOf
- getOwnPropertyDescriptor
改變預設值為0
在其他語言中,如果訪問物件中沒有的屬性,預設會返回0,這在某些場景下很有用,很方便,比如座標系,一般來說z軸預設是0.
不過可以使用proxy解決這個問題
const defaultValueObj = (target, defaultValue) => new Proxy(target, {
get: (obj, k) => Reflect.has(obj, k) ? obj[k] : defaultValue
})
建議根據不同型別返回不同的預設值,Number => 0 String => '' Object => {} Array => []等等
陣列負索引取值
js中,獲取陣列的最後一個元素是相對麻煩的,容易出錯的。這就是為什麼TC39提案定義一個方便的屬性,Array.lastItem
其他語言比如python,和ruby提供了訪問陣列最後一個元素的方法,例如使用arr[-1]代替arr[arr.length - 1]
不過,我們有proxy,負索引在js中也可以實現。
const negativeArray = els => new Proxy(els, {
get: (target, k) => Reflect.get(target, +k < 0 ? String(target.length + +k) : k)
})
需要注意的一點是,get操作會字串化所有的操作,所以我們需要轉換成number在進行操作,
這個運用也是negative-array
的原理
隱藏屬性
js未能實現私有屬性,儘管之後引入了Symbol
去設定獨一無二的屬性,但是這個被後來的Object.getOwnPropertySumbols
淡化了
長期以來,人們使用下劃線_來表示屬性的私有,這意味著不執行外部操作該屬性。不過,proxy提供了一種更好的方法來實現類似的私有屬性
const enablePrivate = (target, prefix = '_') => new Proxy(target, {
has: (obj, k) => (!k.startsWith(prefix) && k in obj),
ownKeys: (obj, k) => Reflece.ownKeys(obj).filter(k => (typeof k !== 'string' || !k.startsWith(prefix))),
get: (obj, k, rec) => (k in rec) ? obj[k] : undefined
})
結果
let userData = enablePrivate({
firstName: 'Tom',
mediumHandle: '@tbarrasso',
_favoriteRapper: 'Drake'
})
userData._favoriteRapper // undefined
('_favoriteRapper' in userData) // false
Object.keys(userData) // ['firstName', 'mediumHandle']
如果你列印該proxy代理物件,會在控制檯看到,不過無所謂。
快取失效
服務端和客戶端同步一個狀態可能會出現問題,這很常見,在整個操作週期內,資料都有可能被改變,並且很難去掌握需要重新同步的時機。
proxy提供了一種新的辦法,可以讓屬性在必要的時候失效,所有的訪問操作,都會被檢查判斷,是否返回快取還是進行其他行為的響應。
const timeExpired = (target, ttl = 60) => {
const created_at = Date.now();
const isExpired = () => (Date.now - created_at) > ttl * 1000;
return new Proxy(tarvet, {
get: (target, k) => isExpired() ? undefined : Reflect.get(target, k);
})
}
上面的功能很簡單,他在一定時間內正常返回訪問的屬性,當超出ttl時間後,會返回undefined。
let timeExpired = ephemeral({
balance: 14.93
}, 10)
console.log(bankAccount.balance) // 14.93
setTimeout(() => {
console.log(bankAccount.balance) // undefined
}, 10 * 1000)
上面的例子會輸出undefined在十秒後,更多的騷操作還請自行斟酌。
只讀
儘管Object.freeze
可以讓物件變得只讀,但是我們可以提供更好的方法,讓開發者在操作屬性的時候獲取明確的提示
const nope = () => {
throw new Error('不能改變只讀屬性')
}
const read_only = (obj) => new Proxy(obj, {
set: nope,
defineProperty: nope,
deleteProperty: nope,
preentExtensions: nope,
setPrototypeOf: nope
});
列舉
結合上面的只讀方法
const createEnum = (target) => read_only(new Proxy(target, {
get: (obj, k) = {
if (k in obj) {
return Reflect.get(obj, k)
}
throw new ReferenceError(`找不到屬性${k}`)
}
}))
我們得到了一個物件,如果你訪問不存在的屬性,不會得到undefined,而是丟擲一個指向異常錯誤,折讓除錯變得更方便。
這也是一個代理代理的例子,需要保證被代理的代理是一個合法的代理物件,這個有助於混合一些複雜的功能。
過載操作符
最神奇的可能就是過載某些操作符了,比如使用handler.has
過載in
。
in用來判斷指定的屬性是否指定物件或者物件的原型鏈上,這種行為可以很優雅的被過載,比如建立一個用於判斷目標數字是否在制定範圍內的代理
const range = (min, max) => new Proxy(Object.create(null), {
has: (obj, k) => (+k > min && +k < max)
})
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;
其他
- 相容性一般,不過谷歌開發的proxy-polyfill目前已經支援get、set、apply、construct到ie9了
- 目前瀏覽器沒有辦法判斷物件是否被代理,不過在node版本10以上,可以使用
util.types.isProxy
來判斷 - proxy的第一個引數必須是物件,不能代理原始值
- 效能,proxy的一個缺點就是效能,但是這個也因人/瀏覽器而異,不過,proxy絕對不適合用在效能關鍵點的程式碼上,當然,你可以衡量proxy帶來的遍歷和可能損耗的效能,進行合理的中和,來達到最佳的開發體驗和使用者體驗