元编程

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

可撤销的 Proxy

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

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

js
const revocable = Proxy.revocable( {}, { get(target, name) { return `[[${name}]]`; }, }, ); 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 }