深入理解 Proxy:从原理到实战应用
在前端开发中,Proxy 是 JavaScript 中的一个强大特性,它可以拦截并自定义对象的基本操作(如属性访问、赋值、函数调用等)。Proxy 广泛应用于数据劫持、响应式系统、API 封装等领域,是许多现代框架(如 Vue3、MobX)的核心实现原理之一。
一、Proxy 基础概念
Proxy(代理)是 ES6 引入的一个新特性,它允许你创建一个对象的代理,从而拦截并重新定义该对象的基本操作。
基本语法
const proxy = new Proxy(target, handler);
target
:要代理的目标对象(可以是任何类型的对象,包括数组、函数等)。handler
:一个对象,定义拦截行为(如get
、set
、apply
等)。
基本示例
const target = { name: "Alice" };
const handler = {
get(target, prop) {
console.log(`读取属性 ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`设置属性 ${prop} 为 ${value}`);
target[prop] = value;
return true; // 表示设置成功
},
};
const proxy = new Proxy(target, handler);
proxy.name; // 控制台输出: "读取属性 name"
proxy.age = 25; // 控制台输出: "设置属性 age 为 25"
在这个例子中,我们拦截了 get
和 set
操作,并在操作发生时打印日志。
二、Proxy 的拦截操作
Proxy 的 handler
对象可以定义多种拦截方法,常见的有:
拦截方法 | 触发时机 | 示例 |
---|---|---|
get(target, prop) | 读取属性时触发 | proxy.name |
set(target, prop, value) | 设置属性时触发 | proxy.age = 25 |
has(target, prop) | in 操作符检查属性时触发 | "name" in proxy |
deleteProperty(target, prop) | delete 操作时触发 | delete proxy.name |
apply(target, thisArg, args) | 函数调用时触发 | proxy() |
construct(target, args) | new 操作时触发 | new ProxyClass() |
ownKeys(target) | Object.keys() 等操作时触发 | Object.keys(proxy) |
拦截 get
和 set
const user = { name: "Bob" };
const proxy = new Proxy(user, {
get(target, prop) {
if (prop === "fullName") {
return `${target.name} Smith`; // 计算属性
}
return target[prop];
},
set(target, prop, value) {
if (prop === "age" && value < 0) {
throw new Error("年龄不能为负数");
}
target[prop] = value;
return true;
},
});
console.log(proxy.fullName); // "Bob Smith"
proxy.age = -5; // 抛出错误: "年龄不能为负数"
这里我们:
- 拦截
get
,动态计算fullName
属性。 - 拦截
set
,对age
进行校验。
拦截函数调用(apply
)
function sum(a, b) {
return a + b;
}
const proxySum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`调用函数,参数: ${args}`);
return target(...args) * 2; // 修改返回值
},
});
console.log(proxySum(2, 3)); // 输出: "调用函数,参数: 2,3" → 10
这里我们拦截函数调用,并修改返回值。
三、Proxy 的常见应用场景
数据响应式(如 Vue 3)
Vue 3 使用 Proxy 实现响应式数据:
function reactive(obj) {
return new Proxy(obj, {
get(target, prop) {
console.log(`读取 ${prop}`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`更新 ${prop} 为 ${value}`);
return Reflect.set(target, prop, value);
},
});
}
const state = reactive({ count: 0 });
state.count++; // 输出: "读取 count" → "更新 count 为 1"
数据校验
const validator = {
set(target, prop, value) {
if (prop === "age" && typeof value !== "number") {
throw new Error("年龄必须是数字");
}
target[prop] = value;
return true;
},
};
const person = new Proxy({}, validator);
person.age = "30"; // 抛出错误: "年龄必须是数字"
缓存(Memoization)
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("缓存命中");
return cache.get(key);
}
const result = target(...args);
cache.set(key, result);
return result;
},
});
}
const fibonacci = memoize((n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2)));
console.log(fibonacci(10)); // 第一次计算
console.log(fibonacci(10)); // 输出: "缓存命中"
日志记录
const withLogging = (obj) => {
return new Proxy(obj, {
get(target, prop) {
console.log(`读取属性: ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`设置属性: ${prop} = ${value}`);
target[prop] = value;
return true;
},
});
};
const loggedObj = withLogging({});
loggedObj.name = "Alice"; // 输出: "设置属性: name = Alice"
console.log(loggedObj.name); // 输出: "读取属性: name" → "Alice"
四、Proxy 的局限性
- 无法拦截某些操作:
===
、instanceof
等操作无法被 Proxy 拦截。
- 性能开销:
- Proxy 比直接操作对象稍慢,但现代 JS 引擎已优化得很好。
- 目标对象必须存在:
- 不能代理原始值(如
number
、string
),只能代理对象。
- 不能代理原始值(如