技术实践

关于ES6中的Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(met

羊先生的头像
羊先生2020.02.18 · 10 分钟阅读 · 60 阅读
关于ES6中的Proxy封面
文章正文还需 10 分钟

如果你平时有关注 Vue 的进展的话,可能已经知道了在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作

Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程

Proxy是构造函数,它有两个参数targethandler

handler 实例方法

方法 说明
get(target, propKey, receiver) 拦截对象属性的读取,比如proxy.foo和proxy['foo']
set(target, propKey, value, receiver) 拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值
has(target, propKey) 拦截propKey in proxy的操作,返回一个布尔值
deleteProperty(target, propKey) 拦截delete proxy[propKey]的操作,返回一个布尔值
ownKeys(target) 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循
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),返回一个布尔值
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)
js 复制代码
let p = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

拦截读取写入

接下来我们通过 Proxy 来实现一个数据响应式

js 复制代码
let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      //
      if (typeof target[property] === 'object' && target[property] !== null) {
        return new Proxy(target[property], handler);
      } else {
        return Reflect.get(target, property);
      }
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}

let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`监听到属性${property}改变为${v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2

在上述代码中,我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了

Reflect

Reflect是一个内置的对象,它提供拦截 JavaScript 操作的方法。Reflect不是一个函数对象,因此它是不可构造的。Reflect的所有的方法都是静态的就和Math一样,目前它还没有静态属性。
Reflect对象的方法与Proxy对象的方法相同,更多可以参考Javascript中的Reflect对象

拦截 new 命令 construct()

construct方法用于拦截new命令,下面是拦截对象的写法

js 复制代码
construct (target, args, newTarget)
  • 第一个参数是 目标对象
  • 第二个参数是 构造函数的参数对象
  • 第三个参数是 创造实例对象时,new命令作用的构造函数
js 复制代码
var p = new Proxy(function () {}, {
  construct: function(target, args, newTarget) {
    console.log('called: ' + args.join(', '));
    return { value: args[0] * 10 , newTarget};
  }
});
 
let v =  (new p(1)).value // "called: 1"   10
console.log(new p().newTarget === p) // true

Proxy.revocable()

js 复制代码
Proxy.revocable(target, handler);

revocable 返回值是一个对象,他有两个属性,一个是proxy,就是Proxy的实例,另外一个是revoke,类型是函数,用于移除这个代理

js 复制代码
let target = {};
let handler = {
    get(target, key){
        console.log(key);
        return target[key];
    }
};
 
let obj = Proxy.revocable(target, handler);
console.log(obj);   //{proxy: Proxy, revoke: function}
 
obj.proxy.foo = 123;
console.log(obj.proxy.foo); // 123
 
obj.revoke();
console.log(obj.proxy.foo); // TypeError: Revoked

Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问

更多有关文章

ES6中的Proxy、Reflect以及Vue3.0中的应用原理
Proxy
ES6 系列之 defineProperty 与 proxy