Proxy和Reflect的用法简析

儿独

Proxy是ES6的语法。对于ES6的箭头函数、变量的解构赋值,我们都能耳熟能详,熟练使用。对于Proxy 这样的特性却很少用到,一方面考虑到兼容性的问题(babel已经解决了),另一方面确实这些特性不那么容易使用,也就是使用场景不多。

Proxy主要是用来修改某些操作的默认行为,从这个角度来讲,你可以把他理解成一个拦截器。想要访问对象,都要经过这层拦截。那么我们就可以在这层拦截上做各种操作了。比如你设置一个对象的值的时候,对对象的值进行校验等。

Proxy 支持的拦截操作一共 13 种:

  • 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.用来拦截和监听对对象属性的各种操作,可以在各种操作之前做校验或者打日志等。这个很简单了。

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.扩充操作符

使用 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一一对应。

阅读 4.1k

不长写的日志
js,node.js,就这么多
715 声望
25 粉丝
0 条评论
715 声望
25 粉丝
文章目录
宣传栏