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.
但是在js中,對象中不存在的key的默認值是undefined,而不是合法的初始值。
不過可以使用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帶來的遍歷和可能損耗的性能,進行合理的中和,來達到最佳的開發體驗和用戶體驗
