JavaScript 代理和反射

Author: Guang

Proxy 与 Reflect 概述

ProxyReflect 是 ECMAScript 6 (ES6) 中引入的两个重要特性。 Proxy 提供了对对象操作的拦截与定制能力,可以捕获对象的访问、修改、函数调用等行为,从而让开发者在底层控制对象交互。 Reflect 则提供了一组与对象操作相关的静态方法,它的功能大多可以通过传统的 JavaScript API 实现,但在设计上更一致、更规范化,且在未来的语言扩展中更具适应性。 在实践中,Reflect 常与 Proxy 搭配使用,使拦截逻辑更简洁、更符合语义。

Reflect 是 ES6 引入的内置对象,它促进了JavaScript代码的清晰和一致性,同时,Reflect 是解决 Proxy 使用过程中出现的 this 指向错误不可或缺的方法。

Proxy 与属性描述符的对比

人们常说 “Proxy 比属性描述符更强大”,但这种说法并不严谨。两者虽然都能影响对象的访问与修改,但实现机制与职责完全不同

以下是两者的对比:

  1. 拦截范围与底层约束

    • Proxy 拦截的操作类型更丰富;而属性描述符是对象属性机制的基础。
    • Proxy 的行为必须遵守属性描述符的不变式(trap invariants),否则会抛出 TypeError
  2. 拦截层级

    • 属性描述符只能定义属性的访问与修改行为,无法干预对象方法的执行。
    • Proxy 可以捕获方法调用等高级操作,例如拦截数组的 pushunshift 等。
  3. 实现机制

    • Proxy 是通过创建代理对象来实现的,外部代码只与代理对象交互,不直接操作原对象。
    • 属性描述符则直接作用于原对象本身,调用方持有的仍是原对象引用。

Proxy

Proxy() 构造函数用于创建一个代理对象,它接收两个参数:

  • target:要代理的原始对象;
  • handler:包含捕获器(trap)的捕获器对象,用于定义拦截行为。

语法如下:

const proxy = new Proxy(target, handler)
  • target —— 是要包装的对象,可以是任何东西,包括函数。
  • handler —— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如 get 捕捉器用于读取 target 的属性,set 捕捉器用于写入 target 的属性,等等。

handler中可定义的13种可选的捕获器如下:

捕获器 对应操作
get() 属性读取操作的捕捉器
set() 属性设置操作的捕捉器。
has() in 操作符的捕捉器。
getPrototypeOf() Object.getPrototypeOf 方法的捕捉器。
setPrototypeOf() Object.setPrototypeOf 方法的捕捉器。
isExtensible() Object.isExtensible 方法的捕捉器。
preventExtensions() Object.preventExtensions 方法的捕捉器。
getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor 方法的捕捉器。
defineProperty() Object.defineProperty 方法的捕捉器。
deleteProperty() delete 操作符的捕捉器。
ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
apply() 函数调用操作的捕捉器。
construct() new 操作符的捕捉器。

Proxy 基础示例

使用get和set捕获器记录对象的访问和验证对象的更改。

const personInstance = {
  name: 'Alice',
  age: 25
}

const person = new Proxy(personInstance, {
  // 拦截属性读取
  get(obj, prop) {
    console.log(`读取属性:${prop}`)
    return Reflect.get(obj, prop)
  },

  // 拦截属性写入
  set(obj, prop, value) {
    console.log(`设置属性:${prop} = ${value}`)

    // 如果是修改 age 属性,进行合法性校验
    if (prop === 'age') {
      if (typeof value !== 'number' || value <= 0 || !Number.isInteger(value)) {
        throw new TypeError('属性 age 必须是正整数')
      }
    }

    // 使用 Reflect.set 保持标准行为
    return Reflect.set(obj, prop, value)
  }
})

// 示例输出
console.log(person.name)   // 读取属性:name → Alice

person.age = 30            // 设置属性:age = 30
console.log(person.age)    // 读取属性:age → 30

person.name = 'Bob'        // 设置属性:name = Bob
console.log(person.name)   // 读取属性:name → Bob

// 触发验证错误
try {
  person.age = -5
} catch (e) {
  console.error(e.message) // 输出:属性 age 必须是正整数
}

捕获器不变式

通过使用 Proxy,JavaScript 支持干预访问和修改对象的行为,但这些干预不可以破坏语言功能的正确性或者一致性,这就是 JavaScript 的不变式。更多内容可查询规范。 Proxy 中支持的每一个捕获器,都需要不违反各自的不变式,否则 JavaScript 将抛出 TypeError

各个捕获器介绍

以下逐一介绍捕获器拦截的操作和不变式的内容表现。

1. get() 捕获器及不变式

拦截的操作:

  • 属性访问:proxy[foo], proxy.bar
  • 继承属性访问:Object.create(proxy)[foo]Reflect.get()

捕获器不变式:

  • 如果对应于 target 的属性是不可写且不可配置的数据属性,那么该属性值必须与其相同。
  • 如果对应于 target 的属性是不可配置的访问器属性,且其 [[Get]] 属性为 undefined,那么该属性值必须为 undefined

示例 当 Proxy 捕获器返回的值与目标对象上定义的属性描述符不一致时(例如属性是不可写、不可配置的),就会违反 捕获器不变式(trap invariant),从而导致 TypeError。

// 创建目标对象
const target = {}

// 定义一个不可写且不可配置的属性
Object.defineProperty(target, 'constant', {
  value: 42,
  writable: false,
  configurable: false
})

// 定义 Proxy 并拦截 get 操作
const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`读取属性:${prop}`)
    // 故意返回一个不同的值,违反不变式
    return 100
  }
})

// 尝试读取属性
try {
  console.log(proxy.constant)
} catch (e) {
  console.error(e) // TypeError: 'get' on proxy: property 'constant' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value
}

2. set() 捕获器及不变式

拦截的操作:

  • 属性赋值:proxy[foo] = barproxy.foo = bar
  • 继承属性赋值:Object.create(proxy)[foo] = barReflect.set()

捕获器不变式:

  • 如果对应于 target 的属性是不可写且不可配置的数据属性,那么就不能修改该属性的值使其不同于 target 上对应属性的值。
  • 如果对应于 target 的属性是不可配置的访问器属性,且其 [[Set]] 属性为 undefined,那么就不能设置该属性的值。
  • 在严格模式下,如果 set 捕获器返回 false,则会抛出 TypeError 异常。

示例 设置属性值违反了不变式 → 抛出 TypeError;

'use strict'

// 创建目标对象
const target = {}

// 定义一个不可写且不可配置的数据属性
Object.defineProperty(target, 'fixed', {
  value: 10,
  writable: false,
  configurable: false
})

// 创建代理,强行尝试修改固定属性
const proxy = new Proxy(target, {
  set(obj, prop, value) {
    console.log(`尝试设置属性:${prop} = ${value}`)
    // 故意返回 true,但值不同,违反不变式
    obj[prop] = value
    return true
  }
})

// 尝试修改属性
try {
  proxy.fixed = 20
} catch (e) {
  console.error(e)
  // TypeError: 'set' on proxy: trap returned truish for property 'fixed'
  // which is non-writable and non-configurable in the proxy target
}

3. has() 捕获器及不变式

拦截的操作:

  • 属性查询:foo in proxy
  • 继承属性查询:foo in Object.create(proxy)Reflect.has()

捕获器不变式:

  • 如果存在一个对应于 target 的属性是不可配置的自有属性,那么该属性不能被报告为不存在的。
  • 如果存在一个对应于 target 的属性是自有属性,且 target 不可扩展,那么该属性不能被报告为不存在的。

示例

// 创建目标对象
const target = { secret: 42 }

// 将属性定义为不可配置
Object.defineProperty(target, 'secret', {
  configurable: false,
  enumerable: true,
  writable: true
})

// 创建代理,试图隐藏不可配置属性
const proxy = new Proxy(target, {
  has(obj, prop) {
    console.log(`检查属性是否存在:${prop}`)
    // 故意返回 false,违反不变式
    return false
  }
})

// 测试属性查询
try {
  console.log('secret' in proxy)
} catch (e) {
  console.error(e)
  // TypeError: 'has' on proxy: property 'secret' cannot be reported as non-existent
  // because it is non-configurable in the proxy target
}

4. getPrototypeOf() 捕获器及不变式

拦截的操作:

  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • __proto__
  • Object.prototype.isPrototypeOf()
  • instanceof

捕获器不变式:

  • getPrototypeOf 方法必须返回一个对象或 null
  • 如果 target 不可扩展,Object.getPrototypeOf(proxy) 必须返回和 Object.getPrototypeOf(target) 一样的值。

示例

const target = {};
Object.preventExtensions(target); // 使 target 不可扩展

const proxy = new Proxy(target, {
  getPrototypeOf(target) {
    console.log('尝试返回不同的原型');
    return {}; // ❌ 与 target 的原型不一致
  }
});

try {
  Object.getPrototypeOf(proxy);
} catch (e) {
  console.error(e);
  // TypeError: 'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype
}

5. setPrototypeOf() 捕获器及不变式

拦截的操作:

  • Object.setPrototypeOf()
  • Reflect.setPrototypeOf()

捕获器不变式:

  • 如果 target 不可扩展,参数 prototype 必须与 Object.getPrototypeOf(target) 的值相同。

示例

const target = {};
Object.preventExtensions(target); // target 不可扩展
const proto = Object.getPrototypeOf(target);

const proxy = new Proxy(target, {
  setPrototypeOf(target, prototype) {
    console.log('尝试设置不同的原型');
    return true; // 虽然返回 true,但违反不变式
  }
});

try {
  Object.setPrototypeOf(proxy, { x: 1 }); // ❌ 不变式被破坏
} catch (e) {
  console.error(e);
  // TypeError: 'setPrototypeOf' on proxy: trap returned truish for setting a prototype different from the target's extensible state
}

// ✅ 合法情况:prototype 与原始相同
Object.setPrototypeOf(proxy, proto); // 不会报错

6. isExtensible() 捕获器及不变式

拦截的操作:

  • Object.isExtensible()
  • Reflect.isExtensible()

捕获器不变式:

  • Object.isExtensible(proxy) 必须返回和 Object.isExtensible(target) 一样的值。

示例

const target = {};
Object.preventExtensions(target);

const proxy = new Proxy(target, {
  isExtensible(target) {
    console.log('违反不变式:目标不可扩展,但返回 true');
    return true; // ❌ 与目标不一致
  }
});

try {
  console.log(Object.isExtensible(proxy));
} catch (e) {
  console.error(e);
  // TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target
}

7. preventExtensions() 捕获器及不变式

拦截的操作:

  • Object.preventExtensions()
  • Reflect.preventExtensions()

捕获器不变式:

  • 如果 Object.isExtensible(proxy) 值为 false,那么 Object.preventExtensions(proxy) 只可能返回 true

示例

const target = {};
const proxy = new Proxy(target, {
  preventExtensions(target) {
    console.log('违反不变式:未实际禁止扩展却返回 true');
    // ❌ 错误做法:没有真的禁止扩展
    return true;
  }
});

try {
  Object.preventExtensions(proxy);
} catch (e) {
  console.error(e);
  // TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is still extensible
}

8. getOwnPropertyDescriptor() 捕获器及不变式

拦截的操作:

  • Object.getOwnPropertyDescriptor()
  • Reflect.getOwnPropertyDescriptor()

捕获器不变式:

  • getOwnPropertyDescriptor 必须返回对象或者 undefined
  • 如果存在一个对应于 target 的属性是不可配置的自有属性,那么该属性不能被报告为不存在的。
  • 如果存在一个对应于 target 的属性是自有属性,且该 target 不可扩展,那么该属性不能被报告为不存在的。
  • 如果并不存在一个对应于 target 的属性是自有属性,且该 target 不可扩展,那么该属性不能被报告为存在的。
  • 如果并不存在一个对应于 target 的属性是自有属性,或存在一个对应于 target 的属性是可配置的自有属性,那么它不能被报告为不可配置的。
  • Object.getOwnPropertyDescriptor(target) 的结果可以通过 Object.defineProperty 应用到 target 上,且不会抛出异常。

示例

const target = {};
Object.defineProperty(target, 'secret', {
  value: '123',
  configurable: false,
});

const proxy = new Proxy(target, {
  getOwnPropertyDescriptor(target, prop) {
    console.log('违反不变式:不可配置属性被报告为不存在');
    return undefined; // ❌ 错误:隐藏了不可配置属性
  }
});

try {
  Object.getOwnPropertyDescriptor(proxy, 'secret');
} catch (e) {
  console.error(e);
  // TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurable property 'secret' as non-existent
}

9. defineProperty() 捕获器及不变式

拦截的操作:

  • Object.defineProperty()
  • Reflect.defineProperty()

捕获器不变式:

  • 如果 target 不可扩展,那么就不能添加属性。
  • 如果并不存在一个对应于 target 的属性是不可配置的自有属性,那么就不能添加(或修改)该属性为不可配置的。
  • 如果存在一个对应于 target 的属性是可配置的,那么这个属性未必是不可配置的。
  • 如果存在一个对应于 target 的属性,那么 Object.defineProperty(target, prop, descriptor) 将不会抛出异常。
  • 在严格模式下,如果 defineProperty 处理器返回 false,则会抛出 TypeError 异常。

示例

'use strict';

// 创建目标对象,并不可扩展
const target = {};
Object.preventExtensions(target);

const proxy = new Proxy(target, {
  defineProperty(target, prop, descriptor) {
    console.log(`尝试定义属性: ${prop}`);
    // ❌ 错误做法:返回 true,但实际上无法在不可扩展对象上添加属性
    return true;
  }
});

try {
  Object.defineProperty(proxy, 'age', { value: 25 });
} catch (e) {
  console.error(e);
  // TypeError: 'defineProperty' on proxy: trap returned truish for defining property 'age' on a non-extensible object
}

10. deleteProperty() 捕获器及不变式

拦截的操作:

  • 属性删除:delete proxy[foo]delete proxy.fooReflect.deleteProperty()

捕获器不变式:

  • 如果存在一个对应于 target 的属性是不可配置的自有属性,那么该属性不能被删除。

示例

'use strict';

// 创建目标对象
const target = {};
Object.defineProperty(target, 'secret', {
  value: 42,
  configurable: false, // 不可配置
});

const proxy = new Proxy(target, {
  deleteProperty(target, prop) {
    console.log(`尝试删除属性: ${prop}`);
    // ❌ 错误做法:返回 true,但实际上不可配置属性无法删除
    return true;
  }
});

try {
  delete proxy.secret;
} catch (e) {
  console.error(e);
  // TypeError: 'deleteProperty' on proxy: trap returned truish for property 'secret' which is non-configurable
}

11. ownKeys() 捕获器及不变式

拦截的操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • Reflect.ownKeys()

捕获器不变式:

  • ownKeys 的返回值是一个数组。
  • 返回值中的每个元素类型为 StringSymbol
  • 返回值中必须包含 target 的所有不可配置自有属性的键名。
  • 如果 target 不可扩展,那么返回值中必须有且仅有 target 的所有自有属性的键名。

示例

'use strict';

// 创建目标对象
const target = {};
Object.defineProperty(target, 'id', {
  value: 1,
  configurable: false, // 不可配置
});
Object.defineProperty(target, 'name', {
  value: 'Alice',
  configurable: true,
});

// 创建代理
const proxy = new Proxy(target, {
  ownKeys(target) {
    console.log('ownKeys 捕获器被触发');
    // ❌ 错误做法:返回数组,但缺少不可配置属性 'id'
    return ['name'];
  }
});

try {
  console.log(Object.keys(proxy)); // 触发 ownKeys
} catch (e) {
  console.error(e);
  // TypeError: 'ownKeys' on proxy: trap must include all non-configurable own keys
}

12. apply() 捕获器及不变式

拦截的操作:

  • proxy(..args)
  • Function.prototype.apply()
  • Function.prototype.call()
  • Reflect.apply()

捕获器不变式:

  • 不存在关于 handler.apply 方法的不变式。

13. construct() 捕获器及不变式

拦截的操作:

  • new proxy(...args)
  • Reflect.construct()

捕获器不变式:

  • 返回值必须是一个 Object 对象。

示例

'use strict';

// 目标构造函数
function Person(name) {
  this.name = name;
}

// 创建代理
const proxy = new Proxy(Person, {
  construct(target, args, newTarget) {
    console.log(`construct 捕获器被触发: args = ${args}`);
    // ❌ 错误做法:返回非对象
    return "Not an object";
  }
});

try {
  const p = new proxy('Alice'); // 触发 construct
} catch (e) {
  console.error(e);
  // TypeError: 'construct' on proxy: trap returned non-object ('Not an object')
}

可撤销代理

可以用 Proxy.revocable() 方法来创建可撤销的 Proxy 对象。这意味着可以通过 revoke 函数来撤销并关闭一个代理。 此后,对代理进行的任意的操作都会导致 TypeError

示例如下:

'use strict';

// 创建可撤销代理
const target = { name: 'Alice' };
const { proxy, revoke } = Proxy.revocable(target, {
  get(obj, prop) {
    console.log(`访问属性: ${prop}`);
    return obj[prop];
  },
  set(obj, prop, value) {
    console.log(`设置属性: ${prop} = ${value}`);
    obj[prop] = value;
    return true;
  }
});

// 使用代理
console.log(proxy.name); // 输出:访问属性: name → Alice
proxy.age = 25;          // 输出:设置属性: age = 25
console.log(proxy.age);  // 输出:访问属性: age → 25

// 撤销代理
revoke();

// 任何对 proxy 的操作都会抛出 TypeError
try {
  console.log(proxy.name);
} catch (e) {
  console.error(e);
  // TypeError: Cannot perform 'get' on a proxy that has been revoked
}

try {
  proxy.age = 30;
} catch (e) {
  console.error(e);
  // TypeError: Cannot perform 'set' on a proxy that has been revoked
}

Reflect

Reflect 是 ES6 引入的内置对象,它提供了一组静态方法,用于操作对象,功能与 Proxy handler 的方法相同。特点:

  1. 不可构造:不能通过 new Reflect() 创建实例。
  2. 静态方法:所有方法都是静态的(类似 Math 对象)。
  3. 函数式风格:API 设计更一致、返回值明确,更利于函数式编程。
  4. 与 Proxy 搭配:Proxy 捕获器内部常使用 Reflect 方法,保证行为一致。

Reflect 不是一个函数对象,因此它是不可构造的,不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。以下是13个 Reflect 集合方法:

Reflect 方法 对应行为
Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值,类似于 target[name]
Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf()
Reflect.setPrototypeOf(target, prototype) 设置对象原型的函数。返回一个 Boolean,如果更新成功,则返回 true
Reflect.isExtensible(target) 类似于 Object.isExtensible().
Reflect.preventExtensions(target) 类似于 Object.preventExtensions()。返回一个Boolean
Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined
Reflect.defineProperty(target, propertyKey, attributes) Object.defineProperty() 类似。如果设置成功就会返回 true
Reflect.deleteProperty(target, propertyKey) 作为函数的delete操作符,相当于执行 delete target[name]
Reflect.ownKeys(target) 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受 [[enumerable]] 影响).
Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
Reflect.construct(target, argumentsList[, newTarget]) 对构造函数进行 new 操作,相当于执行 new target(...args)

Reflect 的优势

  1. 一致性:方法命名与操作符对应,API 直观易懂。

    const obj = { a: 1 };
    Reflect.has(obj, 'a'); // true,等价于 'a' in obj
    
  2. 返回值清晰:失败时通常抛出异常,而不是返回 falseundefined

    const target = {};
    Object.preventExtensions(target);
    try {
      Reflect.defineProperty(target, 'x', { value: 1 });
    } catch(e) {
      console.error(e); // TypeError
    }
    
  3. 适合函数式编程:可作为函数调用,无副作用。

  4. 与 Proxy 协作:捕获器中通常用 Reflect 进行默认操作,保证不破坏对象原有行为。

内置方法与 Reflect 的关系

JavaScript 的底层对象操作依赖 “内部方法”(如 [[Get]][[Set]][[HasOwnProperty]] 等)。Reflect 将这些内部方法以显式接口暴露:

var myObject = Object.create(null);
console.log(myObject.hasOwnProperty); // undefined

// 传统写法
Object.prototype.hasOwnProperty.call(myObject, 'foo'); // false

// 使用 Reflect
Reflect.has(myObject, 'foo'); // false

为什么不直接挂在 Object 上?

  1. Reflect 方法不仅适用于对象,也适用于函数(如 Reflect.apply)。
  2. 使用单一对象存放反射方法,避免污染全局和构造函数原型。
  3. 保持语言的简洁性和向后兼容性,避免新增关键字。typeofinstanceof 以及 delete 已经作为反射运算符存在了 —— 为此添加同样功能的新关键字将会加重开发者的负担,同时,对于向后兼容性也是一个梦魇,并且会让 JavaScript 中的保留字数量急速膨胀。

与 Proxy 配合使用的 Reflect 必要性:修复 this 指向

细心的读者会发现,Proxy 捕获器和 Reflect 静态方法中相对应的 receiver 参数。receiver 参数代表属性访问的最终对象,也就是 Proxy 代理本身或继承访问的对象。我们需要通过 receiver 参数修复代理对象对原型链上的 getter/setter 调用中对 this 指向的破坏。

在 Proxy 的 getset 捕获器中,直接访问 target[prop] 或赋值可能导致 继承链行为与 this 指向异常。这是因为:

  1. getter/setter 的 this:访问或修改属性时,如果该属性是原型链上的 getter 或 setter,内部的 this 默认绑定到 receiver(即操作的最终对象)。
  2. 继承链访问:当对象通过 Object.create(proxy) 创建子对象访问属性时,如果不显式传入 receiverthis 将绑定到 target,导致继承行为被破坏。

示例:不使用 Reflect 导致 this 错误

const proto = {
  get greeting() {
    return `Hello, ${this.name}`;
  }
};

const obj = Object.create(proto);
obj.name = 'Alice';

const proxy = new Proxy(obj, {
  get(target, prop) {
    return target[prop]; // ❌ this 指向 target,而不是访问者
  }
});

const child = Object.create(proxy);
child.name = 'Bob';

console.log(child.greeting); // Hello, undefined ❌

getter 内部的 this 绑定到了 target,而不是 child。使用 Reflect 可以修复 this 的指向,Reflect.get 可以接受第三个参数 receiver,用来指定 getter 内部的 this 指向:

const proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver); // ✅ this 指向正确
  }
});

console.log(child.greeting); // Hello, Bob ✅

同理,对于 set 捕获器:

const proxy = new Proxy(obj, {
  set(target, prop, value, receiver) {
    return Reflect.set(target, prop, value, receiver); // 保持 setter 内 this 正确
  }
});

在 Proxy 中使用 Reflect,不只是为了简化代码,更是为了保证 this 指向正确、继承链访问一致以及不破坏原有对象行为的规范方式。

发表留言

历史留言

--空--