JavaScript 中的属性代理与元编程
字数 957 2025-12-06 23:44:03
JavaScript 中的属性代理与元编程
JavaScript 的元编程能力允许我们在运行时修改对象的默认行为。属性代理是元编程的核心技术之一,它通过 Proxy 对象拦截对目标对象的操作,从而实现自定义行为。下面我会逐步讲解属性代理的基本概念、使用场景和实现细节。
1. 什么是属性代理?
属性代理是指创建一个代理对象,它可以拦截并重新定义对目标对象的基本操作。这些操作包括读取属性、赋值、函数调用、枚举属性等。Proxy 是 ES6 引入的全局构造函数,用于创建这样的代理对象。
基本结构:
const proxy = new Proxy(target, handler);
target:要代理的目标对象。handler:一个包含“捕获器”(trap)的对象,用于定义拦截行为。
2. 常见的捕获器(trap)及其作用
捕获器是 handler 对象中定义的方法,用于拦截特定操作。以下是几个核心捕获器:
(1)get:拦截属性读取
const target = { name: "Alice" };
const handler = {
get(obj, prop) {
console.log(`读取属性: ${prop}`);
return prop in obj ? obj[prop] : "属性不存在";
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 输出:"读取属性: name" → "Alice"
console.log(proxy.age); // 输出:"读取属性: age" → "属性不存在"
(2)set:拦截属性赋值
const handler = {
set(obj, prop, value) {
if (prop === "age" && typeof value !== "number") {
throw new TypeError("age 必须是数字");
}
obj[prop] = value;
return true; // 表示赋值成功
}
};
const proxy = new Proxy({}, handler);
proxy.age = 25; // 成功
proxy.age = "25"; // 抛出 TypeError
(3)has:拦截 in 操作符
const handler = {
has(obj, prop) {
console.log(`检查属性是否存在: ${prop}`);
return prop in obj;
}
};
const proxy = new Proxy({ a: 1 }, handler);
console.log("a" in proxy); // 输出:检查属性是否存在: a → true
(4)apply:拦截函数调用
当代理目标为函数时,可以拦截调用操作:
const target = function (a, b) { return a + b; };
const handler = {
apply(func, thisArg, args) {
console.log(`调用函数,参数: ${args}`);
return func(...args) * 2;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy(3, 4)); // 输出:调用函数,参数: 3,4 → 14
(5)construct:拦截 new 操作
class Person {
constructor(name) { this.name = name; }
}
const handler = {
construct(Target, args) {
console.log(`创建实例,参数: ${args}`);
return new Target(...args);
}
};
const ProxyPerson = new Proxy(Person, handler);
const p = new ProxyPerson("Bob"); // 输出:创建实例,参数: Bob
3. 属性代理的常见应用场景
(1)数据验证与保护
通过 set 捕获器,确保赋值符合规则(如类型检查、范围验证):
const validator = {
set(obj, prop, value) {
if (prop === "score" && (value < 0 || value > 100)) {
throw new RangeError("分数必须在 0~100 之间");
}
obj[prop] = value;
return true;
}
};
let student = new Proxy({}, validator);
student.score = 85; // 成功
student.score = 120; // 抛出 RangeError
(2)动态计算属性
通过 get 捕获器,实现“虚拟属性”:
const handler = {
get(obj, prop) {
if (prop === "fullName") {
return `${obj.firstName} ${obj.lastName}`;
}
return obj[prop];
}
};
const person = new Proxy({ firstName: "John", lastName: "Doe" }, handler);
console.log(person.fullName); // 输出:John Doe
(3)日志与调试
记录对象的操作历史:
const loggerHandler = {
get(obj, prop) {
console.log(`GET ${prop}`);
return obj[prop];
},
set(obj, prop, value) {
console.log(`SET ${prop}=${value}`);
obj[prop] = value;
return true;
}
};
const data = new Proxy({}, loggerHandler);
data.x = 10; // 输出:SET x=10
console.log(data.x); // 输出:GET x → 10
(4)负索引数组
模拟 Python 风格的负索引访问数组:
const negativeIndexHandler = {
get(arr, prop) {
let index = Number(prop);
if (index < 0) index = arr.length + index;
return arr[index];
}
};
const arr = [10, 20, 30];
const proxyArr = new Proxy(arr, negativeIndexHandler);
console.log(proxyArr[-1]); // 输出:30
4. 注意事项与限制
- 性能开销:代理操作比直接操作稍慢,在性能关键场景需谨慎使用。
- 不可代理的限制:某些内置对象(如
Date、Map)的部分内部方法无法被拦截。 - 目标对象不变性:代理不会修改目标对象本身,所有操作通过代理转发。
this绑定:在代理中,this可能指向代理对象而非目标对象,需注意上下文。
5. 与 Reflect 的配合使用
Reflect 对象提供了与捕获器同名的方法,可用于在代理中方便地调用默认行为:
const handler = {
get(obj, prop) {
console.log(`读取: ${prop}`);
return Reflect.get(obj, prop); // 调用默认的 get 行为
}
};
const proxy = new Proxy({ a: 1 }, handler);
console.log(proxy.a); // 输出:读取: a → 1
通过属性代理,开发者可以灵活控制对象行为,实现数据验证、日志、虚拟属性等高级功能。掌握 Proxy 和 Reflect 是深入 JavaScript 元编程的关键一步。