Proxy和Reflect原来这么简单

308 阅读4分钟

Proxy 代理

讲讲API

const p = new Proxy(target, handler)

target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

Proxy 常用用法

var handler = {
  //读取
  get: function (target, name, receiver) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  //设置
  set (target, propKey, value, receiver) {
    if (propKey.includes('_')) {
      throw new TypeError('it is private');
    }
    obj[prop] = value;
    return true;
  },

  //拦截函数调用
  apply: function (target, thisBinding, args) {
    return args[0];
  },

  //拦截new
  construct: function (target, args) {
    return { value: args[1] };
  },

  //拦截HasProperty操作
  has: function (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }

  //其他拦截如delete,setPrototypeOf, setPrototypeOf等都是一些api的拦截
};


var proxyObj = new Proxy(function (x, y) {
  return x + y;
}, handler);

proxyObj(1, 2) // 拦截函数,默认返回第一项 1
new proxyObj(1, 2) // 拦截构造函数    {value: 2}
proxyObj.prototype === Object.prototype //改造原型对象 true
proxyObj.foo === "Hello, foo" //读取操作默认浅比较  true

var privateObj = new Proxy({
  _context: { data: 'data' }
}, handler);

privateObj._context = { data: null } // _ 私有属性不能设置

if (data in privateObj._context) {  //has拦截
  console.log('exist _context');
}

Proxy 简单来解释就一个拦截器,你可以监听任意对象的属性修改,在修改前你可以实现你自己的逻辑,那么你也就可以靠自己实现如:

  1. 禁止不合法的属性名修改
  2. 实现各种数据类型的原型对象的方法重写
  3. 用于数据的映射转换、常见的如前端的接口返回数据转换、ORM操作数据库

Proxy 的this问题

在proxy中的this不是目标对象的透明代理,无法保证与目标对象一致, 目标对象内部的this关键字会指向 Proxy 代理, 而不是Proxy本身,这意味着proxy内部的this是不受控制的,代理的对象随时都可能被其他拦截操作而改变,影响实际结果。而Reflect就能很好的解决this问题。

const _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane'); 
jane.name // 'Jane'

const proxy = new Proxy(jane, {});  //指向的是_name
proxy.name // 此时访问proxy,但是this已经指向了_name, 所以undefined

Reflect 反射

Reflect的api,跟proxy都能对应上,要说复刻都不为过

Reflect.apply(target, thisArg, args)  
Reflect.construct(target, args)  
Reflect.get(target, name, receiver)  
Reflect.set(target, name, value, receiver)  
Reflect.defineProperty(target, name, desc)   //老版本的拦截
Reflect.deleteProperty(target, name)   //delete 关键字
Reflect.has(target, name)   //in关键字
Reflect.ownKeys(target)   //返回对象的所有属性
Reflect.isExtensible(target)   //对象是否可扩展
Reflect.preventExtensions(target)   //操作让对象不可扩展,返回是否已修改的结果
Reflect.getOwnPropertyDescriptor(target, name)   //获取原型
Reflect.getPrototypeOf(target)  //获取原型
Reflect.setPrototypeOf(target, prototype)   //设置原型

通过观察者模式的简单实现来引出 Reflect 的用法


//观察者模式
const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, { set });

//vue响应式系统的底层增删改查的属性的改变都是通过Proxy拦截和Reflect操作对象,每个响应式的deps的map集合,deps有对应的不同的副作用函数effectFn的set集合,然后做遍历执行。
function set (target, key, value, receiver) {
  //receiver 处理this始终执行proxy,而不是target
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

//被观察目标对象
const person = observable({
  name: '张三',
  age: 20
});

function print () {
  console.log(`${person.name}, ${person.age}`)
}

//观察person
observe(print);
person.name = '李四';

上面的例子是要person.name改变,那么Proxy的代理后,就会触发set,那么我们之前用Proxy的时候set内部其实用了对象内的原型方法,在这个例子我们却用了Reflect,本质来说我们用Reflect模拟Object的一些api操作,包括this的解决。还有一个需要注意的是,proxy的拦截的api跟reflect拦截api都是一一对应的。所以你可以简单理解在每个proxy拦截操作时,你都需要用Reflect来处理对象属性操作的逻辑。

// proxy的set拦截老的判断
'assign' in Object // true

// proxy的set拦截新的判断
Reflect.has(Object, 'assign') // true

从上面的代码能明显感觉到相比Object的各种表达式,Reflect的函数式写法代码可读性其实更好。当然和Object操作还有其他一些细微的差别....

// 在无法定义属性时,会抛出一个错误, 多余的try catch !
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法默认返回false, 是不是更好些?
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

总结

Reflect可以看做解决了Proxy的this指向问题,同时更好的兼容和代码可读性成为了Proxy拦截操作对象的绝佳搭档。Proxy解决了defineProperty原来的监听的局限性带来的性能缺陷。同时更多的拦截api的优势,让vue框架底层使用Proxy、Reflect来实现响应式系统。