1. 程式人生 > >es6 proxy淺析

es6 proxy淺析

Proxy

使用proxy,你可以把老虎偽裝成貓的外表,這有幾個例子,希望能讓你感受到proxy的威力。
proxy 用來定義自定義的基本操作行為,比如查詢、賦值、列舉性、函式呼叫等。

proxy接受一個待代理目標物件和一些包含元操作的物件,為待代理目標建立一個‘屏障’,並攔截所有操作,重定向到自定義的元操作物件上。

proxy通過new Proxy來建立,接受兩個引數:

  1. 待代理目標物件
  2. 元操作物件

閒話少說,直接看例子。

最簡單的只代理一個方功能,在這個例子裡,我們讓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.xtarget[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帶來的遍歷和可能損耗的效能,進行合理的中和,來達到最佳的開發體驗和使用者體驗