Proxy和Reflect的用法簡析

Proxy是ES6的語法。對於ES6的箭頭函數、變量的解構賦值,咱們都能耳熟能詳,熟練使用。對於Proxy 這樣的特性卻不多用到,一方面考慮到兼容性的問題(babel已經解決了),另外一方面確實這些特性不那麼容易使用,也就是使用場景很少。數組

Proxy主要是用來修改某些操做的默認行爲,從這個角度來說,你能夠把他理解成一個攔截器。想要訪問對象,都要通過這層攔截。那麼咱們就能夠在這層攔截上作各類操做了。好比你設置一個對象的值的時候,對對象的值進行校驗等。babel

Proxy 支持的攔截操做一共 13 種:app

  • get(target, propKey, receiver):攔截對象屬性的讀取,好比proxy.foo和proxy['foo']。若是一個屬性不可配置(configurable)和不可寫(writable),則該屬性不能被代理,經過 Proxy 對象訪問該屬性會報錯。
  • set(target, propKey, value, receiver):set方法的第四個參數receiver,老是返回this關鍵字所指向的那個對象,即proxy實例自己。表明攔截對象屬性的設置,好比proxy.foo = v或proxy['foo'] = v,返回一個布爾值。
  • has(target, propKey):攔截propKey in proxy的操做,返回一個布爾值。值得注意的是,has方法攔截的是HasProperty操做,而不是HasOwnProperty操做,即has方法不判斷一個屬性是對象自身的屬性,仍是繼承的屬性。
  • deleteProperty(target, propKey):攔截delete proxy[propKey]的操做,返回一個布爾值。
  • ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一個數組。該方法返回目標對象全部自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
  • getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
  • defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
  • preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
  • getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
  • isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。這個方法有一個強限制,它的返回值必須與目標對象的isExtensible屬性保持一致,不然就會拋出錯誤。
  • setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。若是目標對象是函數,那麼還有兩種額外操做能夠攔截。
  • apply(target, object, args):攔截 Proxy 實例做爲函數調用的操做,好比proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):攔截 Proxy 實例做爲構造函數調用的操做,好比new proxy(...args)。

經過proxy能夠作什麼呢?函數

1.用來攔截和監聽對對象屬性的各類操做,能夠在各類操做以前作校驗或者打日誌等。這個很簡單了。this

2.中斷操做設計

目標對象不容許直接訪問,必須經過代理訪問,一旦訪問結束,就收回代理權,不容許再次訪問。代理

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

3.能夠經過Proxy實現是對象的私有變量(真正的私有)日誌

// filter參數做爲一個過濾函數出入
function priavateProp(obj, filter){
  const handler = {
    get(obj, prop) {
      if(!filter(prop)){
        let val = Reflect.get(obj, prop)
        if(typeof val === 'function'){
          val = val.bind(obj)
        }
        return val
      }
    },
    set(obj, prop, val) {
      if(filter(prop)){
        throw new Error(`cannot set property ${prop}`)
      }
      return Reflect.set(obj, prop, val)
    },
    has(obj, prop) {
      return filter(prop) ? false : Reflect.has(obj, prop)
    },
    ownKeys(obj) {
      return Reflect.ownKeys(obj).filter( prop => !filter(prop))
    }
  }
  return new Proxy(obj, handler)
}
// 私有屬性過濾器 
// 規則:以 _ 爲開頭的屬性都是私有屬性
function filter(prop){
 return prop.indexOf('_') === 0
}

const o = {
 _private: 'private property',
 name: 'public name',
 say(){
 // 內部訪問私有屬性
 console.log(this._private)
 }
}

const p = priavateProp(o, filter)

console.log(p) // Proxy 

p._private // undefined
JSON.stringify(p) // "{"name":"public name"}"

// 只能內部訪問私有屬性
p.say() // private property

console.log('_private' in p) // false

// 不能遍歷到私有屬性 
Object.keys(p) // ["name", "say"]

// 私有屬性不能賦值
p._private = '000' // Uncaught Error: cannot set property _private

4.擴充操做符code

使用 Proxy 但是實現操做符的重載,但也只能對 in of delete new 這幾個實現重載對象

咱們劫持 in 操做符來實現 Array.includes 檢查值是否存在數組中

function arrIn(arr){
 const handler = {
 has(arr, val) {
 return arr.includes(val)
 }
 }

 return new Proxy(arr, handler)
}

const arr = arrIn(['a', 'b', 'c'])

'a' in arr // true

1 in arr // false

5.追蹤數組和對象的變更。聽說下一版本的VUE就是經過這種方式來進行數據綁定的。

function trackChange(target, fn){
  const handler = {
    set(target, key, value) {
      const oldVal = target[key]
      target[key] = value;
      fn(target, key, oldVal, value)
    },
    deleteProperty(target, key) {
      const oldVal = target[key]
      delete target[key]
      fn(target, key, oldVal, undefined)
      return true;
    }
  }

  return new Proxy(target, handler)
}
const obj = trackChange({}, (target, key, oldVal, newVal) => {
  console.log(`obj.${key} value from ${oldVal} to ${newVal}`)
})
obj.a = 'a111' // obj.a value from undefined to a111
obj.a = 'axxxxx' // obj.a value from a111 to axxxxx
delete obj.b // obj.b value from undefined to undefined
obj.c = 'c1' // obj.c value from undefined to c1
const arr = trackChange([1, 2, 3, 4, 5], (target, key, oldVal, newVal) => {
  let val = isNaN(parseInt(key)) ? `.${key}` : `[${key}]`
  const sum = arr.reduce( (p,n) => p + n)

  console.log(`arr${val} value from ${oldVal} to ${newVal}`)
  console.log(`sum [${arr}] is ${sum}`)
})

arr[4] = 0
// arr[4] value from 5 to 0
// sum [1,2,3,4,0] is 10

delete arr[3]
// arr[3] value from 4 to undefined
// sum [1,2,3,,0] is 6

arr.length = 2
// arr.length value from 5 to 2
// sum [1,2] is 3

裏面有些疑點在此披露下:

拿這個deleteProperty(target, propKey)來講,我查了不少資料上說,若是這個方法拋出錯誤或者返回false,當前屬性就沒法被delete命令刪除。(暫時未找到標準上是怎麼解釋的)

var handler = {
  deleteProperty (target, key) {
    console.log('delete '+ key)
    return true
  },
};
function invariant (key, action) {
  if (key[0] === '_') {
    return false;
  }
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
console.log(proxy._prop)
console.log(target._prop)

上面的代碼返回true,並無刪除。其實若是你在這個函數裏面執行了刪除操做,返回什麼並不重要。也就是說這個函數返回值是true,或者false,並不能影響刪除的結果。除非你拋出來錯誤,那這個代碼就執行不了了,直接報錯了。試驗了defineProperty,也是這樣的。這些攔截函數,返回結果是不重要的,重要的是你在攔截相應的操做的時候,你作了什麼操做,好比你攔截的是刪除屬性,那你必需要有刪除屬性這個操做,你要是給這個屬性賦值,那確定達不到效果。這一點要搞清楚。

可是若是代理的是個數組,那他們return的值就必須得是true。

var handler = {
  set(target,key,value,proxy){
    var oldVal = target[key];
    var newVal = value;
    target[key] = value
    console.log(`arr ${key} changed from ${oldVal} to ${newVal}`)
  },
  deleteProperty (target, key) {
    console.log('delete '+ key)
    delete target[key];
  },
};


var arr = new Proxy([1,2,3], handler);
arr.push(2);//'set' on proxy: trap returned falsish for property '3'
arr.pop();//'deleteProperty' on proxy: trap returned falsish for property '2'

這就是set和deleteProperty不返回結果,爆出來的問題。每一個函數中返回true,返回結果就是

arr 3 changed from undefined to 2
arr length changed from 4 to 4
delete 3
arr length changed from 4 to 3

因此無論怎麼樣,對於這類返回boolean類型的方法,咱們必定要記得返回值。

還有一種規範的寫法,咱們通常都是經過Reflect進行攔截操做,而後將其結果返回。

var handler = {
  set(target,key,value,proxy){
    var oldVal = target[key];
    var newVal = value;
    console.log(`arr ${key} changed from ${oldVal} to ${newVal}`)
       return Reflect.set(target,key,value,proxy);
  },
  deleteProperty (target, key) {
    console.log('delete '+ key)
    return Reflect.deleteProperty(target,key)
  },
};


var arr = new Proxy([1,2,3], handler);
arr.push(2);
arr.pop();

那什麼是Reflect呢?

Reflect對象與Proxy對象同樣,也是ES6爲了操做對象而提供的新API。Reflect對象的設計目的有這樣幾個。

  • 將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty),放到Reflect對象上。現階段,某些方法同時在Object和Reflect對象上部署,將來的新方法將只部署在Reflect對象上。
  • 修改某些Object方法的返回結果,讓其變得更合理。好比,Object.defineProperty(obj, name, desc)在沒法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
  • 讓Object操做都變成函數行爲。某些Object操做是命令式,好比name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行爲。
  • Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象能夠方便地調用對應的Reflect方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論Proxy怎麼修改默認行爲,你總能夠在Reflect上獲取默認行爲。

它擁有的API和Proxy一一對應。

相關文章
相關標籤/搜索