Skip to content

Proxy 和 Reflect 对象允许你拦截并自定义基本语言操作(例如属性查找、赋值、枚举和函数调用等)。借助这两个对象,你可以在 JavaScript 进行元级别的编程。

GPT介绍

在 JavaScript 中,元编程指的是通过编程来控制程序的行为。JavaScript 通过提供诸如代理(Proxies)和反射(Reflect)API等工具,使得元编程成为可能。这些工具允许开发者在运行时拦截、修改甚至重定义基本语言操作。

1. 使用 Proxy 进行元编程

Proxy 对象是 ES6 中引入的一种强大的元编程工具,它允许你定义一个对象的行为的自定义行为,比如属性查找、赋值、枚举、函数调用等。

基本用法

一个 Proxy 对象定义了要代理的目标对象和一个处理器对象,该处理器对象是一个包含若干用于拦截操作的方法的普通对象。

js
let target = {};
let handler = {
  get: function(obj, prop) {
    return prop in obj ? obj[prop] : 42;
  }
};

let proxy = new Proxy(target, handler);
console.log(proxy.a);  // 输出 42

在这个例子中,handler 对象定义了一个 get 方法,该方法拦截对 proxy 对象属性的读取。如果属性存在,则返回其值;如果不存在,则返回 42。

动态代理

代理可以动态地创建和管理对象的属性。这对于创建 API 或框架非常有用,可以根据需要实时地修改对象的行为。

js
let handler = {
  get(target, prop, receiver) {
    if (!(prop in target)) {
      target[prop] = new Date().toLocaleTimeString();
    }
    return Reflect.get(target, prop, receiver);
  }
};

let proxy = new Proxy({}, handler);

console.log(proxy.time);  // 输出创建时间
setTimeout(() => console.log(proxy.time), 1000);  // 1秒后输出相同的时间

2. 使用 Reflect 进行元编程

Reflect 是一个内置的对象,它提供了一些用于执行对象默认操作的方法,如属性查找、赋值、操作对象原型等。Reflect 的方法与 Proxy 处理器对象的方法相对应。

js
let obj = { x: 1, y: 2 };
console.log(Reflect.get(obj, 'x'));  // 输出 1
Reflect.set(obj, 'x', 3);
console.log(obj.x);  // 输出 3

使用 Reflect 可以更容易地编写通用的操作,同时保持代码的可读性和标准的行为。

3. 元编程的应用

  • 动态函数调用:可以使用 Reflect.apply() 动态地调用函数。
  • 观察者模式实现:使用 Proxy 可以轻松实现属性的观察者模式,拦截对象的修改并作出响应。
  • 数据绑定和响应式编程:框架如 Vue.js 利用 Proxy 实现数据的响应式绑定。

结论

JavaScript 的元编程功能使开发者能够编写更灵活、更动态的代码,通过控制如何操作对象和函数,可以创建更抽象的层和更智能的应用程序。这些技术特别适合开发复杂的库和框架,为开发者提供强大的工具来控制和扩展语言的核心功能。

代理

Proxy 对象可以拦截某些操作并实现自定义行为。

例如获取一个对象上的属性:

js
let handler = {
  get(target, name) {
    return name in target ? target[name] : 42;
  },
};

let p = new Proxy({}, handler);
p.a = 1;

console.log(p.a, p.b); // 1, 42

Proxy 对象定义了一个 target(这里是一个空对象)和一个实现了 get 陷阱 的 handler 对象。这里,代理的对象在获取未定义的属性时不会返回 undefined,而是返回 42

更多例子参见 Proxy 页面。

术语

在讨论代理的功能时会用到以下术语:

handler 包含陷阱的占位符对象(下译作“处理器”)。

陷阱 提供属性访问的方法(这类似于操作系统中 陷阱 的概念)。

target 代理所虚拟化的对象(下译作“目标”)。它通常用作代理的存储后端。JavaScript 会验证与不可扩展性或不可配置属性相关的不变式。

不变式 实现自定义操作时保持不变的语义称为 不变式 。如果你破坏了处理器的不变式,则会引发 TypeError 异常。

处理器和陷阱

以下表格中总结了 Proxy 对象可用的陷阱。详细的解释和例子请看参考页

处理器 / 陷阱拦截的操作不变式
handler.getPrototypeOf()Object.getPrototypeOf() <br>Reflect.getPrototypeOf() <br>__proto__ <br>Object.prototype.isPrototypeOf() <br>instanceof- getPrototypeOf 方法必须返回一个对象或 null。<br>- 如果 target 不可扩展,Object.getPrototypeOf(proxy) 必须返回和 Object.getPrototypeOf(target) 一样的值。
handler.setPrototypeOf()Object.setPrototypeOf() <br>Reflect.setPrototypeOf()如果 target 不可扩展,参数 prototype 必须与 Object.getPrototypeOf(target) 的值相同。
handler.isExtensible()Object.isExtensible() <br>Reflect.isExtensible()Object.isExtensible(proxy) 必须返回和 Object.isExtensible(target) 一样的值。
handler.preventExtensions()Object.preventExtensions() <br>Reflect.preventExtensions()如果 Object.isExtensible(proxy) 值为 false,那么 Object.preventExtensions(proxy) 只可能返回 true
handler.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor() <br>Reflect.getOwnPropertyDescriptor()- getOwnPropertyDescriptor 必须返回对象或者 undefined。<br>- 如果存在一个对应于 target 的属性是不可配置的自有属性,那么该属性不能被报告为不存在的。<br>- 如果存在一个对应于 target 的属性是自有属性,且该 target 不可扩展,那么该属性不能被报告为不存在的。<br>- 如果并不存在一个对应于 target 的属性是自有属性,且该 target 不可扩展,那么该属性不能被报告为存在的。<br>- 如果并不存在一个对应于 target 的属性是自有属性,或存在一个对应于 target 的属性是可配置的自有属性,那么它不能被报告为不可配置的。<br>- Object.getOwnPropertyDescriptor(target) 的结果可以通过 Object.defineProperty 应用到 target 上,且不会抛出异常。
handler.defineProperty()Object.defineProperty() <br>Reflect.defineProperty()- 如果 target 不可扩展,那么就不能添加属性。<br>- 如果并不存在一个对应于 target 的属性是不可配置的自有属性,那么就不能添加(或修改)该属性为不可配置的。<br>- 如果存在一个对应于 target 的属性是可配置的,那么这个属性未必是不可配置的。<br>- 如果存在一个对应于 target 的属性,那么 Object.defineProperty(target, prop, descriptor) 将不会抛出异常。<br>- 在严格模式下,如果 defineProperty 处理器返回 false,则会抛出 TypeError 异常。
handler.has()属性查询<br><br>foo in proxy<br><br>继承属性查询<br><br>foo in Object.create(proxy) <br>Reflect.has()- 如果存在一个对应于 target 的属性是不可配置的自有属性,那么该属性不能被报告为不存在的。<br>- 如果存在一个对应于 target 的属性是自有属性,且 target 不可扩展,那么该属性不能被报告为不存在的。
handler.get()属性访问<br><br>proxy[foo] <br>proxy.bar<br><br>继承属性访问<br><br>Object.create(proxy)[foo] <br>Reflect.get()- 如果对应于 target 的属性是不可写且不可配置的数据属性,那么该属性值必须与其相同。<br>- 如果对应于 target 的属性是不可配置的访问器属性,且其 [[Get]] 属性为 undefined,那么该属性值必须为 undefined
handler.set()属性赋值<br><br>proxy[foo] = bar <br>proxy.foo = bar<br><br>继承属性赋值<br><br>Object.create(proxy)[foo] = bar <br>Reflect.set()- 如果对应于 target 的属性是不可写且不可配置的数据属性,那么就不能修改该属性的值使其不同于 target 上对应属性的值。<br>- 如果对应于 target 的属性是不可配置的访问器属性,且其 [[Set]] 属性为 undefined,那么就不能设置该属性的值。<br>- 在严格模式下,如果 set 处理器返回 false,则会抛出 TypeError 异常。
handler.deleteProperty()属性删除<br><br>delete proxy[foo] <br>delete proxy.foo <br>Reflect.deleteProperty()如果存在一个对应于 target 的属性是不可配置的自有属性,那么该属性不能被删除。
handler.ownKeys()Object.getOwnPropertyNames() <br>Object.getOwnPropertySymbols() <br>Object.keys() <br>Reflect.ownKeys()- ownKeys 的返回值是一个数组。<br>- 返回值中的每个元素类型为 String 或 Symbol。<br>- 返回值中必须包含 target 的所有不可配置自有属性的键名。<br>- 如果 target 不可扩展,那么返回值中必须有且仅有 target 的所有自有属性的键名。
handler.apply()proxy(..args) <br>Function.prototype.apply() <br>Function.prototype.call() <br>Reflect.apply()不存在关于 handler.apply 方法的不变式。
handler.construct()new proxy(...args) <br>Reflect.construct()返回值必须是一个 Object

可撤销的 Proxy

可以用 Proxy.revocable() 方法来创建可撤销的 Proxy 对象。这意味着可以通过 revoke 函数来撤销并关闭一个代理。

此后,对代理进行的任意的操作都会导致 TypeError

js
const revocable = Proxy.revocable(
  {},
  {
    get(target, name) {
      return `[[$&#123;name&#125;]]`;
    },
  },
);
const proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"

revocable.revoke();

console.log(proxy.foo); // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.foo = 1; // TypeError: Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
console.log(typeof proxy); // "object", `typeof` 不会触发任何陷阱

反射

Reflect 是一个内置对象,它为可拦截的 JavaScript 操作提供了方法。这些方法与代理处理器所提供的方法类似。

Reflect 并不是一个函数对象。

Reflect 将默认操作从处理器转发到 target

以 Reflect.has() 为例,你可以将 in 运算符作为函数:

js
Reflect.has(Object, "assign"); // true

更好的 apply 函数

在不借助 Reflect 的情况下,我们通常使用 Function.prototype.apply() 方法调用一个具有给定 this 值和 arguments 数组(或类数组对象)的函数。

js
Function.prototype.apply.call(Math.floor, undefined, [1.75]);

借助 Reflect.apply,这些操作将变得更加简洁:

js
Reflect.apply(Math.floor, undefined, [1.75]);
// 1;

Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"

Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4

Reflect.apply("".charAt, "ponies", [3]);
// "i"

检查属性定义是否成功

使用 Object.defineProperty,如果成功则返回一个对象,否则抛出一个 TypeError,你可使用 try...catch 块来捕获定义属性时发生的任何错误。因为 Reflect.defineProperty 返回一个布尔值表示的成功状态,你可以在这里使用 if...else 块:

js
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

评论区

欢迎留言、补充或勘误。

xiaoba.blog