JavaScript Proxy 处理器方法全解析:13 个与技巧
在 JavaScript 中,Proxy 是一个极为强大的元编程特性。它允许我们创建一个对象的代理,从而拦截并重新定义该对象的基本操作。而这些操作的拦截逻辑,正是通过 handler 对象 上的处理器方法(handler methods) 来实现的。
你列出的这 13 个方法,涵盖了从属性读取、赋值、删除,到原型链操作、对象扩展性控制等方方面面。理解它们,就等于掌握了 Proxy 的核心。下面,我们逐一剖析每个方法的作用、使用场景以及我个人的一些实践经验。
13 个 handler 方法速览
| 方法 | 触发的操作 |
|---|---|
apply() |
拦截函数调用 proxy(...args)、proxy.call()、proxy.apply() |
construct() |
拦截 new proxy(...args) |
defineProperty() |
拦截 Object.defineProperty() |
deleteProperty() |
拦截 delete proxy[prop] |
get() |
拦截属性读取 proxy[prop] |
getOwnPropertyDescriptor() |
拦截 Object.getOwnPropertyDescriptor() |
getPrototypeOf() |
拦截 Object.getPrototypeOf()、__proto__、instanceof |
has() |
拦截 in 操作符 prop in proxy |
isExtensible() |
拦截 Object.isExtensible() |
ownKeys() |
拦截 Object.keys()、Object.getOwnPropertyNames()、Reflect.ownKeys() |
preventExtensions() |
拦截 Object.preventExtensions() |
set() |
拦截属性赋值 proxy[prop] = value |
setPrototypeOf() |
拦截 Object.setPrototypeOf() |
核心方法详解与实践
1. get() —— 属性读取的守门人
get() 是最常用的处理器方法。它会在读取对象属性时被触发,适合做数据校验、日志记录、默认值返回等操作。
<script>
// 代码号:学习编程,用 get 拦截器实现安全的属性访问
const user = {
name: "李明",
age: 28
};
const userProxy = new Proxy(user, {
get(target, prop, receiver) {
if (prop === "age" && target[prop] < 18) {
return "未成年人信息受保护";
}
// 不存在的属性返回友好提示
return prop in target ? target[prop] : `属性 "${prop}" 不存在`;
}
});
console.log(userProxy.name); // 李明
console.log(userProxy.age); // 28(正常返回,因为大于18)
console.log(userProxy.email); // 属性 "email" 不存在
</script>
个人见解:使用 get 做数据验证比在业务代码中到处写 if 判断要干净得多。我在一个用户信息管理系统里,用 get 统一处理敏感字段的访问权限,代码维护量大幅降低。
2. set() —— 赋值时的校验员
set() 在给属性赋值时触发,非常适合做数据格式校验、取值范围限制、只读属性保护。
<script>
// 代码号:学习编程,用 set 拦截器保护数据完整性
const product = {
price: 0,
name: ""
};
const productProxy = new Proxy(product, {
set(target, prop, value, receiver) {
if (prop === "price") {
if (typeof value !== "number" || value < 0) {
console.error("价格必须是正数");
return false; // 赋值失败
}
}
if (prop === "name") {
if (typeof value !== "string" || value.trim() === "") {
console.error("名称不能为空");
return false;
}
}
target[prop] = value;
return true;
}
});
productProxy.price = 99.9; // 正常
productProxy.name = "机械键盘"; // 正常
productProxy.price = -10; // 价格必须是正数(赋值被阻止)
console.log(productProxy.price); // 仍然是 99.9
</script>
3. apply() —— 函数的中间件
apply() 拦截函数的调用,可以用来做性能监控、参数校验、缓存结果等。我常用它来做 API 请求的缓存层。
<script>
// 代码号:学习编程,用 apply 拦截器实现函数调用缓存
function expensiveCalculation(n) {
console.log("正在计算...");
return n * 2;
}
const cachedProxy = new Proxy(expensiveCalculation, {
cache: new Map(),
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (this.cache.has(key)) {
console.log("从缓存返回结果");
return this.cache.get(key);
}
const result = target.apply(thisArg, args);
this.cache.set(key, result);
return result;
}
});
console.log(cachedProxy(5)); // 正在计算... 10
console.log(cachedProxy(5)); // 从缓存返回结果 10
</script>
4. construct() —— 构造函数的守卫
construct() 拦截 new 操作符,可以用来做单例模式、参数转换、实例校验。
<script>
// 代码号:学习编程,用 construct 实现单例模式
let instance = null;
class DatabaseConnection {
constructor(config) {
this.config = config;
}
}
const ConnectionProxy = new Proxy(DatabaseConnection, {
construct(target, args) {
if (!instance) {
instance = new target(...args);
}
return instance;
}
});
const conn1 = new ConnectionProxy({ host: "localhost" });
const conn2 = new ConnectionProxy({ host: "remote" });
console.log(conn1 === conn2); // true,始终返回同一个实例
console.log(conn1.config); // { host: "localhost" },第二次参数被忽略
</script>
5. deleteProperty() —— 删除操作的控制
拦截 delete 操作,可以防止误删重要属性。
<script>
// 代码号:学习编程,用 deleteProperty 保护关键属性
const config = {
apiKey: "12345",
endpoint: "https://api.example.com"
};
const protectedConfig = new Proxy(config, {
deleteProperty(target, prop) {
if (prop === "apiKey") {
console.error("apiKey 不允许删除");
return false;
}
delete target[prop];
return true;
}
});
delete protectedConfig.apiKey; // apiKey 不允许删除
delete protectedConfig.endpoint; // 成功
console.log(protectedConfig); // { apiKey: "12345" }
</script>
6. has() —— in 操作符的过滤器
拦截 prop in proxy 操作,可以用来隐藏某些属性。
<script>
// 代码号:学习编程,用 has 拦截器隐藏内部属性
const userData = {
_password: "secret123",
username: "alice"
};
const safeUser = new Proxy(userData, {
has(target, prop) {
// 以下划线开头的属性对 in 操作符不可见
if (prop.startsWith("_")) {
return false;
}
return prop in target;
}
});
console.log("username" in safeUser); // true
console.log("_password" in safeUser); // false
</script>
7. ownKeys() —— 枚举属性的过滤
拦截 Object.keys()、Reflect.ownKeys() 等操作,控制哪些属性会被枚举。
<script>
// 代码号:学习编程,用 ownKeys 控制属性枚举
const data = {
id: 1,
name: "报表",
_internal: "敏感数据"
};
const filteredProxy = new Proxy(data, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith("_"));
}
});
console.log(Object.keys(filteredProxy)); // ['id', 'name']
// 注意:_internal 在枚举中消失了,但直接访问 filteredProxy._internal 依然可以
</script>
8. defineProperty() 与 getOwnPropertyDescriptor()
这两个方法分别拦截属性定义和属性描述符的获取,配合使用可以实现动态属性行为。
<script>
// 代码号:学习编程,动态控制属性描述符
const dynamicObj = new Proxy({}, {
defineProperty(target, prop, descriptor) {
console.log(`正在定义属性: ${prop}`);
return Reflect.defineProperty(target, prop, descriptor);
},
getOwnPropertyDescriptor(target, prop) {
console.log(`正在获取属性描述符: ${prop}`);
return Reflect.getOwnPropertyDescriptor(target, prop);
}
});
Object.defineProperty(dynamicObj, "version", {
value: "1.0.0",
writable: false
});
// 输出: 正在定义属性: version
const desc = Object.getOwnPropertyDescriptor(dynamicObj, "version");
// 输出: 正在获取属性描述符: version
console.log(desc.value); // 1.0.0
</script>
9. 原型相关:getPrototypeOf() 与 setPrototypeOf()
拦截原型读取和设置,可以伪造原型链或阻止原型修改。
<script>
// 代码号:学习编程,控制原型访问
const base = { type: "base" };
const target = { name: "target" };
const protoProxy = new Proxy(target, {
getPrototypeOf() {
return base; // 假装原型是 base
},
setPrototypeOf() {
console.log("禁止修改原型");
return false;
}
});
console.log(Object.getPrototypeOf(protoProxy) === base); // true
Object.setPrototypeOf(protoProxy, {}); // 禁止修改原型
</script>
10. 扩展性控制:isExtensible() 与 preventExtensions()
这两个方法控制对象的扩展性行为,可以覆盖默认的冻结逻辑。
<script>
// 代码号:学习编程,自定义扩展性判断
const sealed = new Proxy({}, {
isExtensible() {
return false; // 告诉外界对象不可扩展
},
preventExtensions() {
console.log("尝试阻止扩展");
return true; // 表示操作成功
}
});
console.log(Object.isExtensible(sealed)); // false
Object.preventExtensions(sealed); // 尝试阻止扩展
</script>
本节课程知识要点
-
善用
Reflect:在 handler 方法内部,推荐使用Reflect对象上的同名方法来执行原始操作。这样既保持了默认行为,又能在其基础上添加自定义逻辑。例如Reflect.get(target, prop, receiver)。 -
返回值很重要:
set()、defineProperty()、deleteProperty()等方法需要返回布尔值来表示操作是否成功。返回false或在严格模式下抛出异常,可以阻止操作。 -
ownKeys()与枚举的局限:通过ownKeys()过滤掉的属性,在Object.keys()中不会出现,但Reflect.ownKeys()返回的是过滤后的结果,且直接属性访问依然有效。如需隐藏,还需配合get()和has()。 -
性能考量:Proxy 会带来额外的性能开销。在性能敏感的场景(如循环内高频访问)需谨慎使用。我通常在配置层、权限控制层、缓存层使用 Proxy,而在核心计算循环中避免。
这 13 个 handler 方法共同构成了 JavaScript 元编程的基石。掌握它们,你就能在对象层面实现日志、校验、缓存、权限控制、数据隐藏等高级功能。代码号学习编程的过程中,建议从 get 和 set 开始练习,逐步扩展到其他方法,你会发现 Proxy 带来的设计自由度远超预期。