Proxy 与 Reflect 概述
Proxy 和 Reflect 是 ECMAScript 6 (ES6) 中引入的两个重要特性。 Proxy 提供了对对象操作的拦截与定制能力,可以捕获对象的访问、修改、函数调用等行为,从而让开发者在底层控制对象交互。 Reflect 则提供了一组与对象操作相关的静态方法,它的功能大多可以通过传统的 JavaScript API 实现,但在设计上更一致、更规范化,且在未来的语言扩展中更具适应性。 在实践中,Reflect 常与 Proxy 搭配使用,使拦截逻辑更简洁、更符合语义。
Reflect 是 ES6 引入的内置对象,它促进了JavaScript代码的清晰和一致性,同时,Reflect 是解决 Proxy 使用过程中出现的 this 指向错误不可或缺的方法。
Proxy 与属性描述符的对比
人们常说 “Proxy 比属性描述符更强大”,但这种说法并不严谨。两者虽然都能影响对象的访问与修改,但实现机制与职责完全不同。
以下是两者的对比:
-
拦截范围与底层约束
- Proxy 拦截的操作类型更丰富;而属性描述符是对象属性机制的基础。
- Proxy 的行为必须遵守属性描述符的不变式(trap invariants),否则会抛出
TypeError。
-
拦截层级
- 属性描述符只能定义属性的访问与修改行为,无法干预对象方法的执行。
- Proxy 可以捕获方法调用等高级操作,例如拦截数组的
push、unshift等。
-
实现机制
- 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] = bar,proxy.foo = bar - 继承属性赋值:
Object.create(proxy)[foo] = bar,Reflect.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.foo,Reflect.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的返回值是一个数组。- 返回值中的每个元素类型为
String或Symbol。 - 返回值中必须包含
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 的方法相同。特点:
- 不可构造:不能通过
new Reflect()创建实例。 - 静态方法:所有方法都是静态的(类似 Math 对象)。
- 函数式风格:API 设计更一致、返回值明确,更利于函数式编程。
- 与 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 的优势
-
一致性:方法命名与操作符对应,API 直观易懂。
const obj = { a: 1 }; Reflect.has(obj, 'a'); // true,等价于 'a' in obj -
返回值清晰:失败时通常抛出异常,而不是返回
false或undefined。const target = {}; Object.preventExtensions(target); try { Reflect.defineProperty(target, 'x', { value: 1 }); } catch(e) { console.error(e); // TypeError } -
适合函数式编程:可作为函数调用,无副作用。
-
与 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 上?
- Reflect 方法不仅适用于对象,也适用于函数(如
Reflect.apply)。 - 使用单一对象存放反射方法,避免污染全局和构造函数原型。
- 保持语言的简洁性和向后兼容性,避免新增关键字。
typeof、instanceof以及delete已经作为反射运算符存在了 —— 为此添加同样功能的新关键字将会加重开发者的负担,同时,对于向后兼容性也是一个梦魇,并且会让 JavaScript 中的保留字数量急速膨胀。
与 Proxy 配合使用的 Reflect 必要性:修复 this 指向
细心的读者会发现,Proxy 捕获器和 Reflect 静态方法中相对应的 receiver 参数。receiver 参数代表属性访问的最终对象,也就是 Proxy 代理本身或继承访问的对象。我们需要通过 receiver 参数修复代理对象对原型链上的 getter/setter 调用中对 this 指向的破坏。
在 Proxy 的 get、set 捕获器中,直接访问 target[prop] 或赋值可能导致 继承链行为与 this 指向异常。这是因为:
- getter/setter 的
this:访问或修改属性时,如果该属性是原型链上的 getter 或 setter,内部的this默认绑定到receiver(即操作的最终对象)。 - 继承链访问:当对象通过
Object.create(proxy)创建子对象访问属性时,如果不显式传入receiver,this将绑定到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 指向正确、继承链访问一致以及不破坏原有对象行为的规范方式。