« 回到博客列表

每天一点ES6(13):Proxy 和 Reflect

Dec 20th, 2017阅读本文大约需要 5 分钟

写在前面

又好久没更了,这次来讲的是 ES6 中比较冷门的一块内容:元编程。大部分时候其实我们不会接触到这一块,日常的业务开发通常不会涉及这么高级的操作,但元编程对于一门语言来说可谓是瑞士军刀,一旦掌握,便可以施展出强大的法力。

元编程是个什么鬼?

一般我们编程都是为了实现一些业务上的需求,直接使用语言原生提供的内容即可,最多在其上做一层简单的封装。而元编程则是对编程语言本身进行编程,去修改其原本的行为,实现一些原生方法做不到的事,但实际编写起来又是原生的语法,非常神奇。

Proxy

Proxy 本意是代理,一般我们在计算机网络中经常听到这个词。使用 Proxy 我们可以通过编程来接管外界对对象的操作,改变其原本的行为。有点类似于中间件的概念。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

如上的代码就对对象 obj 的读取和设置操作进行了接管,在其原有行为之前加上了一些内容。

Proxy 的基本使用为: const proxy = new Proxy(target, handler) ,target 为被代理的对象,handler 中定义了拦截的行为。

Proxy 实例可以作为对象的原型:

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

这里 obj 本身并没有 time 属性,因此沿原型链向上寻找,找到 proxy 的 time 属性,proxy 虽然也没有 time 属性,但其拦截了所有属性访问操作并固定返回 35,因此依然能拿到结果。

Proxy 支持的拦截操作

操作 作用
get(target, key, receiver) 拦截对象属性的读取,三个参数分别为:目标对象、属性名、proxy 实例本身(即 this 所指),其中最后一个参数可选
set(target, key, value, receiver) 拦截对象属性的设置,返回布尔值
has(target, key) 拦截 key in obj 的操作,返回布尔值
deleteProperty(target, key) 拦截 delete obj[key] 操作,返回一个布尔值
ownKeys(target) 拦截 Object.getOwnPropertyNames(obj)Object.getOwnPropertySymbols(obj)Object.keys(obj) 操作,返回一个数组,包含目标对象所有自身的属性的属性名。 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, key) 拦截 Object.getOwnPropertyDescriptor(obj, key) ,返回属性的描述对象
defineProperty(target, key) 拦截 Object.defineProperty(obj, key, desc)Object.defineProperties(obj, key, descs) ,返回一个布尔值
preventExtensions(target) 拦截 Object.preventExtensions(obj) ,返回一个布尔值
getPrototypeOf(target) 拦截 Object.getPrototypeOf(obj) ,返回一个对象
isExtensible(target) 拦截 Object.isExtensible(obj) ,返回一个布尔值
setPrototypeOf(target, prototype) 拦截 Object.setPrototypeOf(obj, prototype) ,返回一个布尔值,如果目标对象是函数那么还有两种额外操作可以拦截。
apply(target, obj, args) 拦截 Proxy 实例作为函数调用的操作,例如: proxy(...args)proxy.call(object, ...args)proxy.apply(...)
construct(target, args) 拦截 Proxy 实例作为构造函数调用的操作,例如: new proxy(...args)

Proxy.revocable()

Proxy.revocable() 返回一个对象,该对象的 proxy 属性是 Proxy 实例, revoke 是一个用于取消 Proxy 实例的函数。被取消的 Proxy 实例不可再被访问。

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

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

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

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

Proxy.revocable() 的一个使用场景就是临时授权对象访问。一个对象不允许直接访问,但允许通过代理临时访问,访问结束后收回代理权,之后不再允许访问。

Proxy 中的 this

在使用了 Proxy 的情况下,目标对象内部的 this 关键字会指向 Proxy 代理,而非原本的对象。使用时要时刻注意这一点,Proxy 内部的 this 未必是我们以为的那一个

Reflect

字面意思解叫“反射”,熟悉 Java 的朋友应该也有接触过这个词。它和 Proxy 一样,都是 ES6 引入的用于操作对象的新 API。

Reflect 的设计目的有以下几个:

  1. Object 中一些明显属于语言内部的方法放到 Reflect 对象上,例如: Object.defineProperty
  2. 合理化一些 Object 方法的返回结果,例如: Object.defineProperty 在无法定义属性是会抛出错误,而 Reflect.defineProperty 只是返回 false
  3. Object 中命令式的操作都变成函数式。例如: key in objdelete obj[key] 是命令式的, Reflect.has(obj, key)Reflect.deleteProperty(obj, key) 就是函数式的。
  4. Reflect 中的方法与Proxy中的一一对应,在 Proxy 中始终可以通过 Reflect 获取默认行为。

简而言之, Reflect 的存在意义有两点:一是把一些原本捆绑在 Object 对象上但实际上更应该属于语言内部的方法提取出来,并加以合理化,让 Object 更纯粹作为一种数据结构。二是和 Proxy 组 CP,用于在 Proxy 中始终指向默认行为,防止加了代理之后回不去。

Reflect 的静态方法

如前面所说的, Reflect 的静态方法和 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)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

小结

通过 Proxy 和 Reflect,我们可以进一步控制对象的行为,甚至改变其默认行为。这可以说是对 JavaScript 能力的又一次提升。

该系列的其他文章