# 代理和反射

# Proxy

一个 Proxy 对象包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理它们。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。

Proxy 被用于了许多库和某些浏览器框架。

# 概述

基本语法

const proxy = new Proxy(target, handler);
  • target 要使用Proxy包装的目标对象
  • handler 一个通常以函数作为属性的对象,各函数分别定义了在执行各种操作时代理proxy的行为

对 proxy 进行操作,如果在 handler 中存在相应的捕捉器,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。

let target = {};
let proxy = new Proxy(target, {}); // 空的 handler 对象

proxy.test = 5; // 写入 proxy 对象 (1)
alert(target.test); // 5,test 属性出现在了 target 中!

alert(proxy.test); // 5,我们也可以从 proxy 对象读取它 (2)

for (let key in proxy) alert(key); // test,迭代也正常工作 (3)

由于没有捕捉器,所有对 proxy 的操作都直接转发给了 target。

  • 写入操作 proxy.test= 会将值写入 target。
  • 读取操作 proxy.test 会从 target 返回对应的值。
  • 迭代 proxy 会从 target 返回对应的值。

# 拦截操作

get(target, propKey, receiver) 拦截读取
set(target, propKey, value, receiver) 拦截设置
has(target, propKey) 拦截in操作
deleteProperty(target, propKey) 拦截 delete
ownKeys(target) 拦截遍历
getOwnPropertyDescriptor(target, propKey)
definProperty(target, propKey, propDesc)
preventExtensions(target)
getPrototypeOf(target)
isExtensible(target)
setPrototypeOf(target, proto)
apply(target, thisBind, args) 拦截()
construct(target, args) 拦截new
const handler = {
  get: (target, propKey, receiver) => {
    console.log("proxy get");
    if (propKey == "prototype") {
      return Object.prototype;
    } else {
      return "key:" + propKey;
    }
  },
  set: (target, propKey, propValue, receiver) => {
    console.log("proxy set");
  },
  apply: (target, thisBind, args) => {
    console.log("proxy apply");
    return args[0];
  },
  construct: (target, args) => {
    console.log("proxy new");
    return {
      value0: args[0],
      value1: args[1]
    };
  }
};
const proxy = new Proxy(function (a, b) {
  console.log(a + b);
  return a + b;
}, handler);
console.log(proxy(1, 2));
// proxy apply
// 1
console.log(proxy.a);
// key:a
console.log(proxy.prototype);
// {}
console.log((proxy.a = 2));
// proxy set
// 2
let p = new proxy(1, 2);
// proxy new
console.log(p);
// { value0: 1, value1: 2 }

# 带有 “get” 捕捉器的默认值

最常见的捕捉器是用于读取/写入的属性。

要拦截读取操作,handler 应该有 get(target, property, receiver) 方法。

读取属性时触发该方法,参数如下:

  • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy,
  • property —— 目标属性名,
  • receiver —— 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。

通常,当人们尝试获取不存在的数组项时,他们会得到 undefined,但是我们在这将常规数组包装到代理(proxy)中,以捕获读取操作,并在没有要读取的属性的时返回 0:

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // 默认值
    }
  }
});

alert(numbers[1]); // 1
alert(numbers[123]); // 0(没有这个数组项)

TIP

代理应该在所有地方都完全替代目标对象。目标对象被代理后,任何人都不应该再引用目标对象。否则很容易搞砸。

# 使用 “set” 捕捉器进行验证

假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误。

let numbers = [];

numbers = new Proxy(numbers, {
  // (*)
  set(target, prop, val) {
    // 拦截写入属性操作
    if (typeof val == "number") {
      target[prop] = val;
      return true; // 请返回true
    } else {
      return false;
    }
  }
});

numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError: 'set' on proxy: trap returned falsish

请注意:数组的内建方法依然有效!值被使用 push 方法添加到数组。当值被添加到数组后,数组的 length 属性会自动增加。我们的代理对象 proxy 不会破坏任何东西。

别忘了返回 true

如上所述,要保持不变量。 对于 set 操作,它必须在成功写入时返回 true。 如果我们忘记这样做,或返回任何假(falsy)值,则该操作将触发 TypeError

# 实例: 使用 Proxy 实现观察者模式

// 设置一个观察者集合
const queuedObservers = new Set();
// 添加观察者(方法)函数
const observe = (fn) => queuedObservers.add(fn);
// 添加观察者函数
const observable = (obj) => new Proxy(obj, { set });

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  // 遍历所有观察者,通知他们(调用他们)
  queuedObservers.forEach((observer) => observer());
  return result;
}
// 定义一个观察者
function print() {
  console.log("观察到了", p.name);
}
// 增加观察者到集合
observe(print);

// 定义一个被观察者
let p = observable({
  name: "法外狂徒张三",
  age: 20
});

// 修改被观察者(实际上是修改代理对象)
p.name = "李四";
// 观察到了 李四;
p.name = "王五";
// 观察到了 王五;

# Reflect

Reflect 对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。设计目的有如下几个

  1. Object对象上的一些明显属于语言内部的方法,放到Reflect对象上。
  2. 修改某些Object方法的返回结果,让其结果变得更合理。
  3. ReflectProxy对象的方法一一对应,也就是说只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

Reflect 是一个内建对象,可简化 Proxy 的创建。

前面所讲过的内部方法,例如 [[Get]][[Set]] 等,都只是规范性的,不能直接调用。

Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。

let user = {};

Reflect.set(user, "name", "John");

alert(user.name); // John

尤其是,Reflect 允许我们将操作符(new,delete,……)作为函数(Reflect.construct,Reflect.deleteProperty,……)执行调用

所以,我们可以使用 Reflect 来将操作转发给原始对象。

在下面这个示例中,捕捉器 get 和 set 均透明地(好像它们都不存在一样)将读取/写入操作转发到对象,并显示一条消息:

let user = {
  name: "John"
};

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`);
    return Reflect.get(target, prop, receiver); // (1)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`);
    return Reflect.set(target, prop, val, receiver); // (2)
  }
});

let name = user.name; // 显示 "GET name"
user.name = "Pete"; // 显示 "SET name=Pete"

这样,一切都很简单:如果一个捕捉器想要将调用转发给对象,则只需使用相同的参数调用 Reflect.<method> 就足够了。

在大多数情况下,我们可以不使用 Reflect 完成相同的事情,例如,用于读取属性的 Reflect.get(target, prop, receiver) 可以被替换为 target[prop]。尽管有一些细微的差别。

# 为什么需要 Reflect

让我们看一个示例,来说明为什么 Reflect.get 更好。此外,我们还将看到为什么 get/set 有第三个参数 receiver,而且我们之前从来没有使用过它。

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// 期望输出:Admin
alert(admin.name); // 输出:Guest (?!?)

发生了什么?或许我们在继承方面做错了什么?

  1. 当我们读取 admin.name 时,由于 admin 对象自身没有对应的的属性,搜索将转到其原型。
  2. 原型是 userProxy。
  3. 从代理读取 name 属性时,get 捕捉器会被触发,并从原始对象返回 target[prop] 属性,在 (*) 行。

当调用 target[prop] 时,若 prop 是一个 getter,它将在 this=target 上下文中运行其代码。因此,结果是来自原始对象 target 的 this._name,即来自 user。

我们可以把捕捉器重写得更短:

get(target, prop, receiver) {
  return Reflect.get(...arguments);
}

Reflect 调用的命名与捕捉器的命名完全相同,并且接受相同的参数。它们是以这种方式专门设计的。

因此,return Reflect... 提供了一个安全的方式,可以轻松地转发操作,并确保我们不会忘记与此相关的任何内容。

# 总结

Proxy 是对象的包装器,将代理上的操作转发到对象,并可以选择捕获其中一些操作。

它可以包装任何类型的对象,包括类和函数。

语法为:

let proxy = new Proxy(target, {
  /* trap */
});

……然后,我们应该在所有地方使用 proxy 而不是 target。代理没有自己的属性或方法。如果提供了捕捉器(trap),它将捕获操作,否则会将其转发给 target 对象。

我们还可以将对象多次包装在不同的代理中,并用多个各个方面的功能对其进行装饰。

Reflect API 旨在补充 Proxy。对于任意 Proxy 捕捉器,都有一个带有相同参数的 Reflect 调用。我们应该使用它们将调用转发给目标对象。