各位开发者,大家好。今天我们将深入探讨 JavaScript Proxy 机制中的核心陷阱(Traps):get、set 和 apply。Proxy 是 ES6 引入的一项强大功能,它允许我们拦截并自定义对对象的基本操作。然而,与任何强大的工具一样,Proxy 也伴随着其自身的复杂性和陷阱,尤其是其规范行为和不变式(Invariants),如果不理解这些,可能会导致难以调试的错误。
本次讲座的目标是让大家透彻理解这三个核心陷阱的默认行为、如何自定义它们,以及最为关键的——它们必须遵守的ECMAScript规范所强制执行的不变式。这些不变式是为了保证语言的内部一致性和安全性,一旦违反,JavaScript 引擎就会抛出 TypeError。
理解 Proxy:拦截与反射
在深入探讨具体的陷阱之前,我们先快速回顾一下 Proxy 的基本概念。
一个 Proxy 对象是使用 new Proxy(target, handler) 构造函数创建的。
target:这是被代理的原始对象。它可以是任何类型的对象,包括函数、数组甚至另一个 Proxy。handler:这是一个包含零个或多个“陷阱”(trap)方法的对象。每个陷阱方法都对应一个可以被 Proxy 拦截的基本操作。
当对 Proxy 对象执行某个操作时(例如读取属性、设置属性或调用函数),如果 handler 中定义了相应的陷阱方法,那么该操作就会被这个陷阱方法拦截并处理。如果 handler 中没有定义相应的陷阱方法,那么这个操作就会“回退”到对 target 对象的默认行为。
为了正确地实现默认行为,ECMAScript 提供了 Reflect API。Reflect 对象提供了一组与 Proxy 陷阱方法同名的静态方法,它们的作用是执行与陷阱方法对应的默认操作。强烈建议在实现 Proxy 陷阱时,如果需要执行默认行为,始终使用 Reflect 对应的方法,而不是直接操作 target。 这是因为 Reflect 方法会正确地处理 this 绑定以及遵守内部规则,从而帮助我们避免违反不变式。
接下来,我们将逐一深入探讨 get、set 和 apply 这三个陷阱。
get 陷阱:属性读取的拦截器
get 陷阱用于拦截对 Proxy 对象的属性读取操作。
1. 陷阱签名与参数
get(target, property, receiver)
target:被代理的原始对象。property:被访问的属性名(字符串或 Symbol)。receiver:最初接收该操作的对象。通常是 Proxy 实例本身,但在继承链中,它可能是继承了 Proxy 的对象。这个参数对于确保在原型链上正确处理this绑定至关重要。
2. 默认行为
如果 handler 中没有定义 get 陷阱,或者 get 陷阱直接返回 Reflect.get(target, property, receiver),那么属性读取操作将表现为:
- 在
target对象上查找property。 - 如果
target拥有该属性,则返回其值。 - 如果
target没有该属性,它将沿着target的原型链向上查找。 - 如果找到一个访问器属性(getter),其
this会被绑定到receiver。
示例:默认 get 行为
const targetObject = {
name: "Alice",
get greeting() {
return `Hello, my name is ${this.name}`;
},
greetMethod() {
return `Method: Hello, my name is ${this.name}`;
}
};
const proxy = new Proxy(targetObject, {}); // 没有定义 get 陷阱
console.log(proxy.name); // 输出: Alice
console.log(proxy.greeting); // 输出: Hello, my name is Alice (this 绑定到 proxy)
console.log(proxy.greetMethod()); // 输出: Method: Hello, my name is Alice (this 绑定到 proxy)
// 使用 Reflect.get 模拟默认行为
const handlerWithReflectGet = {
get(target, property, receiver) {
console.log(`Intercepting get for property: ${String(property)}`);
return Reflect.get(target, property, receiver);
}
};
const proxyWithReflectGet = new Proxy(targetObject, handlerWithReflectGet);
console.log(proxyWithReflectGet.name);
// 输出:
// Intercepting get for property: name
// Alice
console.log(proxyWithReflectGet.greeting);
// 输出:
// Intercepting get for property: greeting
// Hello, my name is Alice
// 继承链中的 receiver 作用
const proto = {
get id() {
return this.value;
}
};
const obj = Object.create(proto);
obj.value = 42;
const proxyProto = new Proxy(proto, {
get(target, prop, receiver) {
console.log(`Proxy proto get: ${prop} for receiver`, receiver);
return Reflect.get(target, prop, receiver);
}
});
const childObj = Object.create(proxyProto);
childObj.value = 100;
console.log(childObj.id);
// 输出:
// Proxy proto get: id for receiver { value: 100 }
// 100
// 在这里,receiver 是 childObj,所以 this.value 解析为 childObj.value,而不是 proto.value。
3. 自定义行为与常见用途
get 陷阱的强大之处在于它允许我们完全控制属性的读取行为。
- 属性访问日志: 记录哪些属性被访问了,这对于调试和监控非常有用。
- 数据验证与转换: 在返回属性值之前对其进行验证或转换。
- 懒加载(Lazy Loading): 只有在第一次访问属性时才计算或获取其值。
- 访问控制/权限管理: 根据某些条件决定是否允许访问特定属性,或者返回不同的值。
- 默认值: 当访问不存在的属性时,返回一个默认值而非
undefined。 - 计算属性: 动态生成属性值。
示例:自定义 get 陷阱
// 1. 属性访问日志与默认值
const data = {
firstName: "John",
lastName: "Doe"
};
const loggedData = new Proxy(data, {
get(target, property, receiver) {
console.log(`Property '${String(property)}' was accessed.`);
if (property === 'fullName') {
return `${Reflect.get(target, 'firstName', receiver)} ${Reflect.get(target, 'lastName', receiver)}`;
}
// 如果属性不存在,返回一个默认的“未定义”提示
if (!(property in target)) {
return `Property '${String(property)}' is not defined.`;
}
return Reflect.get(target, property, receiver);
}
});
console.log(loggedData.firstName); // 输出日志,然后输出 'John'
console.log(loggedData.age); // 输出日志,然后输出 'Property 'age' is not defined.'
console.log(loggedData.fullName); // 输出日志,然后输出 'John Doe'
// 2. 懒加载
const userConfig = new Proxy({}, {
get(target, property, receiver) {
if (property === 'settings' && !Reflect.has(target, 'settings')) {
console.log("Loading user settings...");
// 模拟异步加载
Reflect.set(target, 'settings', {
theme: 'dark',
notifications: true
});
}
return Reflect.get(target, property, receiver);
}
});
console.log(userConfig.settings.theme); // 第一次访问时加载,然后输出 'dark'
console.log(userConfig.settings.notifications); // 第二次访问时不再加载,直接输出 'true'
4. get 陷阱的不变式(Invariants)
get 陷阱必须遵守以下不变式,否则 JavaScript 引擎会抛出 TypeError。这些不变式确保了 Proxy 不会破坏对象属性的某些核心特性。
| 不变式描述 | 违反后果 |
|---|---|
如果 target 对象中存在一个不可配置(non-configurable)且不可写(non-writable)的数据属性 P,那么 get 陷阱返回的值必须与 target[P] 的值相同。 换句话说,你不能通过 Proxy 改变一个不可写属性的值。 |
抛出 TypeError。 |
如果 target 对象中存在一个不可配置(non-configurable)且具有 getter 但没有 setter 的访问器属性 P,那么 get 陷阱返回的值必须与调用该 getter 得到的值相同。 (这实际上是第一个不变式的一个特例,因为如果 getter 存在,它就是属性的“值”)。 |
抛出 TypeError。 |
如果 target 对象中存在一个不可配置(non-configurable)且 [[Get]] 属性为 undefined 的访问器属性 P(即没有 getter),那么 get 陷阱返回的值必须是 undefined。 (这种情况非常罕见,通常如果 [[Get]] 是 undefined,那么 [[Set]] 也不会单独存在。) |
抛出 TypeError。 |
如果 target 对象中存在一个不可配置(non-configurable)且 [[Writable]] 为 false 的数据属性 P,并且 target 上的 P 的 [[Value]] 为 undefined,那么 get 陷阱返回的值必须是 undefined。 (同上,这也是第一个不变式的一个特例,强调了不可写属性的值不能被改变,即使其值是 undefined) |
抛出 TypeError。 |
示例:违反 get 陷阱不变式
const fixedValueObject = {};
Object.defineProperty(fixedValueObject, 'VERSION', {
value: '1.0.0',
writable: false, // 不可写
configurable: false // 不可配置
});
const proxy = new Proxy(fixedValueObject, {
get(target, property, receiver) {
if (property === 'VERSION') {
return '2.0.0'; // 尝试返回一个不同的值
}
return Reflect.get(target, property, receiver);
}
});
try {
console.log(proxy.VERSION); // 尝试读取
} catch (e) {
console.error("Error accessing VERSION:", e.message);
// 输出: Error accessing VERSION: 'get' on proxy: property 'VERSION' is a non-configurable and non-writable data property on the proxy target and the proxy's handler did not return its actual value.
}
const objWithNoGet = {};
Object.defineProperty(objWithNoGet, 'noGetterProp', {
get: undefined, // 没有 getter
set: function() {}, // 有 setter
configurable: false
});
const proxyNoGet = new Proxy(objWithNoGet, {
get(target, property, receiver) {
if (property === 'noGetterProp') {
return 'some value'; // 尝试返回一个非 undefined 的值
}
return Reflect.get(target, property, receiver);
}
});
try {
console.log(proxyNoGet.noGetterProp);
} catch (e) {
console.error("Error accessing noGetterProp:", e.message);
// 输出: Error accessing noGetterProp: 'get' on proxy: property 'noGetterProp' is a non-configurable accessor property on the proxy target with a [[Get]] attribute of undefined, but the proxy's handler did not return undefined.
}
关键点:receiver 的作用
receiver 参数是 Proxy 机制中一个非常重要的设计,尤其是在处理继承和方法调用时。它确保了在原型链上查找属性或调用方法时,this 能够正确地绑定到最初发起操作的对象(通常是 Proxy 实例或其子类实例),而不是 target 或原型链上的其他对象。始终使用 Reflect.get(target, property, receiver) 来代理默认的属性读取行为,可以确保 receiver 参数被正确地传递和使用。
set 陷阱:属性写入的拦截器
set 陷阱用于拦截对 Proxy 对象的属性写入操作。
1. 陷阱签名与参数
set(target, property, value, receiver)
target:被代理的原始对象。property:被设置的属性名(字符串或 Symbol)。value:尝试赋给属性的新值。receiver:最初接收该操作的对象。通常是 Proxy 实例本身,但在继承链中,它可能是继承了 Proxy 的对象。与get陷阱类似,它确保在原型链上正确处理this绑定,尤其是在setter中。
2. 默认行为
如果 handler 中没有定义 set 陷阱,或者 set 陷阱直接返回 Reflect.set(target, property, value, receiver),那么属性写入操作将表现为:
- 在
target对象上查找property。 - 如果
target拥有一个数据属性property:- 如果
property是可写的,则将其值设置为value。 - 如果
property是不可写的,在严格模式下会抛出TypeError,非严格模式下会静默失败。
- 如果
- 如果
target拥有一个访问器属性property并且有setter:- 调用
setter,this会被绑定到receiver,value作为参数传递。
- 调用
- 如果
target没有property,但原型链上有property:- 如果原型链上的
property是一个数据属性:在target上创建property并赋值。 - 如果原型链上的
property是一个访问器属性(有setter):调用原型链上的setter,this绑定到receiver。 - 如果原型链上的
property是一个不可写数据属性,则在严格模式下会抛出TypeError,非严格模式下会静默失败。
- 如果原型链上的
Reflect.set返回一个布尔值,表示设置操作是否成功 (true表示成功,false表示失败)。
示例:默认 set 行为
const targetObject = {
_age: 30,
set age(val) {
console.log(`Setting age to ${val}`);
this._age = val; // this 绑定到 receiver
},
get age() {
return this._age;
}
};
const proxy = new Proxy(targetObject, {}); // 没有定义 set 陷阱
proxy.age = 31;
// 输出: Setting age to 31
console.log(proxy.age); // 输出: 31
// 使用 Reflect.set 模拟默认行为
const handlerWithReflectSet = {
set(target, property, value, receiver) {
console.log(`Intercepting set for property: ${String(property)} with value: ${value}`);
return Reflect.set(target, property, value, receiver); // 总是返回 true/false
}
};
const proxyWithReflectSet = new Proxy(targetObject, handlerWithReflectSet);
proxyWithReflectSet.name = "Bob"; // targetObject 上没有 name 属性
// 输出: Intercepting set for property: name with value: Bob
// targetObject.name 现在是 "Bob"
console.log(targetObject.name); // 输出: Bob
console.log(proxyWithReflectSet.name); // 输出: Bob
proxyWithReflectSet.age = 35;
// 输出:
// Intercepting set for property: age with value: 35
// Setting age to 35
console.log(proxyWithReflectSet.age); // 输出: 35
3. 自定义行为与常见用途
set 陷阱提供了在属性写入时进行拦截和自定义行为的能力。
- 数据验证: 在属性赋值前检查值的有效性,例如类型、范围或格式。
- 数据规范化: 自动转换或清理输入值。
- 变更追踪: 记录属性的更改,例如用于状态管理或撤销/重做功能。
- 副作用: 当属性改变时触发其他操作,例如更新 UI 或发送事件。
- 只读属性/不可变对象: 阻止对特定属性或所有属性的修改。
示例:自定义 set 陷阱
// 1. 数据验证与只读属性
const user = {
id: 1,
name: "Jane",
age: 25
};
const userProxy = new Proxy(user, {
set(target, property, value, receiver) {
if (property === 'id') {
console.warn("Attempted to change ID, which is read-only.");
return false; // 拒绝修改
}
if (property === 'age') {
if (typeof value !== 'number' || value < 0 || value > 120) {
console.error("Invalid age value. Age must be a number between 0 and 120.");
return false; // 拒绝修改
}
}
console.log(`Setting property '${String(property)}' to '${value}'`);
return Reflect.set(target, property, value, receiver); // 允许修改
}
});
console.log("Original user:", userProxy);
userProxy.id = 2; // 警告并拒绝
userProxy.name = "Janet"; // 允许并记录
userProxy.age = 30; // 允许并记录
userProxy.age = -5; // 错误并拒绝
userProxy.age = "abc"; // 错误并拒绝
console.log("Modified user:", userProxy);
// 输出:
// Original user: { id: 1, name: 'Jane', age: 25 }
// Attempted to change ID, which is read-only.
// Setting property 'name' to 'Janet'
// Setting property 'age' to '30'
// Invalid age value. Age must be a number between 0 and 120.
// Invalid age value. Age must be a number between 0 and 120.
// Modified user: { id: 1, name: 'Janet', age: 30 }
// 2. 变更追踪
const trackableObject = {
value: 10
};
const changes = [];
const trackedProxy = new Proxy(trackableObject, {
set(target, property, value, receiver) {
const oldValue = Reflect.get(target, property, receiver);
if (oldValue !== value) {
changes.push({
property: String(property),
oldValue,
newValue: value,
timestamp: new Date()
});
console.log(`Change recorded: ${String(property)} from ${oldValue} to ${value}`);
}
return Reflect.set(target, property, value, receiver);
}
});
trackedProxy.value = 20;
trackedProxy.value = 20; // 不会记录,因为值未改变
trackedProxy.value = 30;
trackedProxy.newValue = 100;
console.log("All changes:", changes);
// 输出:
// Change recorded: value from 10 to 20
// Change recorded: value from 20 to 30
// Change recorded: newValue from undefined to 100
// All changes: [
// { property: 'value', oldValue: 10, newValue: 20, timestamp: ... },
// { property: 'value', oldValue: 20, newValue: 30, timestamp: ... },
// { property: 'newValue', oldValue: undefined, newValue: 100, timestamp: ... }
// ]
4. set 陷阱的不变式(Invariants)
set 陷阱具有非常严格的不变式,因为属性的写入操作直接影响对象的状态和完整性。违反这些不变式将导致 TypeError。
| 不变式描述 | 违反后果 |
|---|---|
set 陷阱必须返回一个布尔值。 true 表示成功,false 表示失败。这是最基本且重要的不变式。 |
抛出 TypeError。 |
如果 target 对象中存在一个不可配置(non-configurable)且不可写(non-writable)的数据属性 P,那么 set 陷阱不能改变 target[P] 的值。 如果 set 陷阱尝试返回 true(表示设置成功),但 target[P] 的值没有改变(或者 set 陷阱尝试返回 false 但 target[P] 的值却改变了),都会抛出 TypeError。简而言之,对于不可写属性,set 陷阱必须要么拒绝改变(返回 false),要么尝试改变但必须失败(返回 true 但 target 上的值未变,这会引发内部检查),但它绝不能导致 target[P] 的值被修改。最安全的做法是对于这样的属性,直接返回 false 或让 Reflect.set 处理(它会抛出 TypeError 或静默失败)。 |
抛出 TypeError。 |
如果 target 对象中存在一个不可配置(non-configurable)且没有 setter 的访问器属性 P(即只有 getter),那么 set 陷阱不能改变 target[P] 的值。 同样,如果 set 陷阱尝试返回 true,但 target[P] 的值在 target 上没有改变,或者 set 陷阱返回 false 但 target[P] 的值却改变了,都会抛出 TypeError。最安全的做法是直接返回 false。 |
抛出 TypeError。 |
如果 target 对象中存在一个不可配置(non-configurable)且具有 setter 的访问器属性 P,那么 set 陷阱必须允许该 setter 被调用。 这意味着 set 陷阱不能返回 false 来阻止该 setter 的执行,除非 setter 本身返回 false(这是不常见的)。通常 set 陷阱应该返回 true,并让 Reflect.set 调用 setter。 |
抛出 TypeError。 |
示例:违反 set 陷阱不变式
// 违反不变式 1: set 陷阱必须返回布尔值
const obj1 = {};
const proxy1 = new Proxy(obj1, {
set(target, property, value, receiver) {
return "not a boolean"; // ❌ 错误:返回非布尔值
}
});
try {
proxy1.test = 1;
} catch (e) {
console.error("Error 1:", e.message);
// 输出: Error 1: 'set' on proxy: trap returned non-boolean value
}
// 违反不变式 2: 改变不可写属性的值
const fixedPropertyObject = {};
Object.defineProperty(fixedPropertyObject, 'ID', {
value: 'abc',
writable: false,
configurable: false
});
const proxy2 = new Proxy(fixedPropertyObject, {
set(target, property, value, receiver) {
if (property === 'ID') {
Reflect.set(target, property, value, receiver); // 尝试修改,但因为是不可写,实际不会修改,Reflect.set会返回false
return true; // ❌ 错误:对于不可写属性,如果值未改变,set陷阱返回true是错误的
}
return Reflect.set(target, property, value, receiver);
}
});
try {
proxy2.ID = 'xyz'; // 尝试设置
} catch (e) {
console.error("Error 2:", e.message);
// 输出: Error 2: 'set' on proxy: property 'ID' is a non-configurable and non-writable data property on the proxy target and the proxy's handler did not return false
}
// 正确处理不可写属性:返回 false
const proxy2Correct = new Proxy(fixedPropertyObject, {
set(target, property, value, receiver) {
if (property === 'ID') {
console.warn("Attempted to modify fixed ID.");
return false; // ✅ 正确:显式拒绝修改
}
return Reflect.set(target, property, value, receiver);
}
});
proxy2Correct.ID = 'xyz'; // 打印警告,但不会抛出错误
console.log("ID after attempt:", fixedPropertyObject.ID); // 仍然是 'abc'
// 违反不变式 3: 改变只有 getter 没有 setter 的属性
const getterOnlyObject = {};
Object.defineProperty(getterOnlyObject, 'readOnlyProp', {
get: () => 'initial',
set: undefined, // 没有 setter
configurable: false
});
const proxy3 = new Proxy(getterOnlyObject, {
set(target, property, value, receiver) {
if (property === 'readOnlyProp') {
// 尝试返回 true,但目标属性无法设置,这将导致 TypeError
return true; // ❌ 错误
}
return Reflect.set(target, property, value, receiver);
}
});
try {
proxy3.readOnlyProp = 'new value';
} catch (e) {
console.error("Error 3:", e.message);
// 输出: Error 3: 'set' on proxy: property 'readOnlyProp' is a non-configurable accessor property on the proxy target with a [[Set]] attribute of undefined, but the proxy's handler did not return false
}
// 正确处理只有 getter 的属性:返回 false
const proxy3Correct = new Proxy(getterOnlyObject, {
set(target, property, value, receiver) {
if (property === 'readOnlyProp') {
console.warn("Attempted to modify getter-only property.");
return false; // ✅ 正确:显式拒绝修改
}
return Reflect.set(target, property, value, receiver);
}
});
proxy3Correct.readOnlyProp = 'new value';
console.log("readOnlyProp after attempt:", getterOnlyObject.readOnlyProp); // 仍然是 'initial'
关键点:receiver 和 Reflect.set
与 get 陷阱类似,set 陷阱中的 receiver 参数同样重要。它确保了在 setter 函数被调用时,this 能够正确地指向 Proxy 实例或其子类实例。使用 Reflect.set(target, property, value, receiver) 不仅确保了这一点,还负责了所有默认的属性设置逻辑,包括原型链上的查找和 setter 的调用,并返回规范要求的布尔值。
apply 陷阱:函数调用的拦截器
apply 陷阱用于拦截对 Proxy 对象的函数调用。值得注意的是,apply 陷阱只有在 target 对象本身是一个函数时才有效。 如果 target 不是函数,那么 Proxy 实例也不能被调用,尝试调用它会直接抛出 TypeError,而不会触发 apply 陷阱。
1. 陷阱签名与参数
apply(target, thisArg, argumentsList)
target:被代理的原始函数。thisArg:调用函数时所使用的this值。argumentsList:一个类数组对象,包含了传递给函数的所有参数。
2. 默认行为
如果 handler 中没有定义 apply 陷阱,或者 apply 陷阱直接返回 Reflect.apply(target, thisArg, argumentsList),那么函数调用操作将表现为:
直接调用 target 函数,this 被绑定到 thisArg,并传入 argumentsList 中的参数。
示例:默认 apply 行为
function greet(name, age) {
return `Hello, my name is ${name} and I am ${age} years old. My context is: ${this ? this.contextName : 'no context'}`;
}
const targetFunction = function(a, b) {
return `Sum: ${a + b}`;
};
const proxy = new Proxy(targetFunction, {}); // 没有定义 apply 陷阱
console.log(proxy(5, 10)); // 输出: Sum: 15
// 使用 Reflect.apply 模拟默认行为
const handlerWithReflectApply = {
apply(target, thisArg, argumentsList) {
console.log(`Intercepting function call with arguments: ${argumentsList}`);
return Reflect.apply(target, thisArg, argumentsList);
}
};
const proxyWithReflectApply = new Proxy(greet, handlerWithReflectApply);
const context = { contextName: "Global Context" };
console.log(proxyWithReflectApply.call(context, "Alice", 30));
// 输出:
// Intercepting function call with arguments: Alice,30
// Hello, my name is Alice and I am 30 years old. My context is: Global Context
console.log(proxyWithReflectApply.apply(context, ["Bob", 25]));
// 输出:
// Intercepting function call with arguments: Bob,25
// Hello, my name is Bob and I am 25 years old. My context is: Global Context
console.log(proxyWithReflectApply("Charlie", 20)); // 直接调用时 thisArg 为 undefined
// 输出:
// Intercepting function call with arguments: Charlie,20
// Hello, my name is Charlie and I am 20 years old. My context is: no context
3. 自定义行为与常见用途
apply 陷阱允许我们完全控制函数的调用行为。
- 函数调用日志: 记录函数被调用的时间、参数和返回值。
- 参数验证: 在函数实际执行前对传入的参数进行检查和验证。
- 权限控制: 根据某些条件决定是否允许函数执行。
- AOP(面向切面编程): 在函数执行前、执行后或异常时插入额外的逻辑(例如性能监控、缓存、事务管理)。
- 错误处理: 统一捕获和处理函数执行中的错误。
- 函数节流/防抖: 限制函数的执行频率。
- 函数柯里化/偏应用: 动态调整函数参数。
示例:自定义 apply 陷阱
// 1. 参数验证与日志
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero!");
}
return a / b;
}
const safeDivide = new Proxy(divide, {
apply(target, thisArg, argumentsList) {
const [a, b] = argumentsList;
console.log(`Calling divide with a=${a}, b=${b}`);
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError("Both arguments must be numbers.");
}
try {
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.error(`Error during division: ${e.message}`);
throw e; // 重新抛出错误
}
}
});
try {
console.log(safeDivide(10, 2)); // 正常执行
console.log(safeDivide(10, 0)); // 抛出 Division by zero!
} catch (e) {
console.error("Caught error:", e.message);
}
try {
console.log(safeDivide("a", 2)); // 抛出 TypeError
} catch (e) {
console.error("Caught error:", e.message);
}
// 2. 简单的函数缓存(Memoization)
function expensiveCalculation(num) {
console.log(`Calculating for ${num}... (expensive operation)`);
// 模拟耗时计算
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += Math.sqrt(i * num);
}
return sum;
}
const memoizedCalculation = new Proxy(expensiveCalculation, {
cache: new Map(), // 存储缓存结果
apply(target, thisArg, argumentsList) {
const key = JSON.stringify(argumentsList); // 使用参数作为缓存键
if (this.cache.has(key)) {
console.log(`Returning cached result for ${argumentsList}`);
return this.cache.get(key);
} else {
const result = Reflect.apply(target, thisArg, argumentsList);
this.cache.set(key, result);
return result;
}
}
});
console.time("calc_10_first");
console.log(memoizedCalculation(10));
console.timeEnd("calc_10_first");
console.time("calc_10_second");
console.log(memoizedCalculation(10)); // 从缓存中获取
console.timeEnd("calc_10_second");
console.time("calc_20_first");
console.log(memoizedCalculation(20));
console.timeEnd("calc_20_first");
4. apply 陷阱的不变式(Invariants)
apply 陷阱的不变式相对简单,主要围绕 target 必须是可调用函数这一核心要求。
| 不变式描述 |
| target 必须是可调用的函数。 否则,在创建 Proxy 实例时(或者在对非函数的 Proxy 实例进行函数调用时)就会抛出 TypeError。 apply 陷阱无法弥补目标对象不是函数的事实。 |
| apply 陷阱不能返回一个非函数值,如果 target 是一个构造函数(即可以被 new 调用的函数)。 (此不变式已在 ES6 后被移除,现在 apply 陷阱对构造函数的返回没有限制。然而,通常的实践是,如果你拦截了一个构造函数,通常会返回一个对象。) | 已被移除。 |
示例:违反 apply 陷阱不变式
// 违反不变式 1: target 不是函数
const notAFunction = {};
try {
const proxyNonFunction = new Proxy(notAFunction, {
apply(target, thisArg, argumentsList) {
console.log("This won't be called.");
return Reflect.apply(target, thisArg, argumentsList);
}
});
proxyNonFunction(); // 尝试调用
} catch (e) {
console.error("Error 1:", e.message);
// 输出: Error 1: proxyNonFunction is not a function
// 这里的 TypeError 是在调用 proxyNonFunction() 时由 JS 引擎抛出,而不是由 apply 陷阱内部抛出。
// 因为 target 本身就不是函数,Proxy 实例也无法被调用。
}
// 示例:正确使用 apply 陷阱包装非构造函数
const myFunc = (a, b) => a + b;
const funcProxy = new Proxy(myFunc, {
apply(target, thisArg, args) {
console.log("Function called with:", args);
return Reflect.apply(target, thisArg, args);
}
});
console.log(funcProxy(1, 2)); // 输出: Function called with: [ 1, 2 ] n 3
// 示例:使用 apply 陷阱包装构造函数 (旧不变式已移除,但仍需注意返回类型)
class MyClass {
constructor(name) {
this.name = name;
}
}
const ClassProxy = new Proxy(MyClass, {
apply(target, thisArg, args) {
console.log("MyClass apply called, thisArg:", thisArg, "args:", args);
// 如果 target 是构造函数,通常这里不会直接调用 Reflect.apply
// 而是期望通过 new 操作符来调用,但如果真的直接调用了,会按普通函数处理。
// 如果想拦截 new 操作,需要使用 construct 陷阱。
// Reflect.apply(target, thisArg, args) 在这里会把 MyClass 当作普通函数调用,
// 而不是构造函数,所以 MyClass 构造函数内部的 this.name = name 不会生效。
return Reflect.apply(target, thisArg, args);
}
});
try {
const instance = ClassProxy("TestName"); // 此时 ClassProxy 被当作普通函数调用
console.log("Instance from apply:", instance); // undefined, 因为 MyClass 作为函数调用没有返回值
} catch (e) {
console.error("Error:", e.message);
}
// 如果要拦截 new 操作,应该使用 construct 陷阱
const ConstructorProxy = new Proxy(MyClass, {
construct(target, args, newTarget) {
console.log("MyClass construct called, args:", args);
// 确保返回一个对象
return Reflect.construct(target, args, newTarget);
}
});
const instance2 = new ConstructorProxy("TestName2");
console.log("Instance from construct:", instance2.name); // 输出: TestName2
关键点:target 必须是函数
apply 陷阱的根本前提是 target 必须是一个可以被调用的函数。如果 target 不是函数,那么 Proxy 实例本身就不是一个可调用的实体,在尝试调用它时就会直接失败,而不会进入 apply 陷阱。因此,在使用 apply 陷阱时,请确保你的 target 是一个函数。
总结:Proxy 陷阱的规范行为与最佳实践
我们已经详细探讨了 get、set 和 apply 这三个核心 Proxy 陷阱的默认行为、自定义能力以及至关重要的不变式。理解这些不变式是编写健壮、符合规范的 Proxy 代码的关键。
get陷阱 拦截属性读取。其核心不变式是不能改变不可配置且不可写的数据属性的值。set陷阱 拦截属性写入。它有最严格的不变式,包括必须返回布尔值,以及不能改变不可配置的不可写属性或只有 getter 没有 setter 的属性的值。apply陷阱 拦截函数调用。其核心前提是target必须是可调用函数。
最佳实践:
- 始终使用
ReflectAPI: 在你的陷阱中,如果你需要执行目标对象的默认行为,请务必使用Reflect对象提供的对应方法(如Reflect.get、Reflect.set、Reflect.apply)。这不仅能确保this绑定(通过receiver或thisArg)的正确性,还能帮助你遵守不变式,因为Reflect方法本身就内置了这些规范检查。 - 理解
receiver和thisArg的作用: 这两个参数对于正确处理继承链和函数执行上下文至关重要。 - 注意返回值的类型:
set陷阱必须返回布尔值。 - 避免意外违反不变式: 当处理不可配置或不可写的属性时,要特别小心。如果你的陷阱试图返回一个与实际目标属性值不符的值,或者试图修改一个不可修改的属性,JavaScript 引擎会立即抛出
TypeError。 - 性能考量: Proxy 引入了一层间接性,必然会带来一些性能开销。在性能敏感的应用中,应谨慎使用 Proxy,并衡量其带来的收益与成本。
- 调试复杂性: Proxy 改变了对象的默认行为,这可能会使调试变得更加复杂。确保你的陷阱逻辑清晰且可预测。
Proxy 是一种非常强大的元编程工具,能够实现许多高级功能,如响应式系统、ORM、AOP等。通过深入理解其规范行为和不变式,我们可以充分利用其潜力,同时避免常见的陷阱,编写出更加健壮和可靠的 JavaScript 代码。