Appearance
4.2 代理模式 (Proxy Pattern)
代理模式是结构型模式中非常重要且功能强大的一员。它旨在为另一个对象提供一个“替身”或“占位符”,以便控制对这个对象的访问。
核心定义:为目标对象创建一个代理对象,以便客户端通过这个代理对象间接地访问目标对象。这样做的好处是可以在不修改目标对象代码的情况下,增加一些额外的逻辑。
一、生活中的场景:明星与经纪人
- 明星 (Real Subject / 真实主体):他是真正会唱歌、演戏的核心人物。但他非常繁忙,而且不希望被外界随意打扰。
- 你想找明星合作 (Client / 客户端):你不能直接联系到明星本人。
- 经纪人 (Proxy / 代理):你只能联系明星的经纪人。经纪人会作为明星的“代理人”与你沟通。
这个“经纪人”做了什么?
- 访问控制:他会帮你过滤掉不合适的请求(比如粉丝的骚扰电话),只把真正重要的工作(比如电影邀约)转达给明星。
- 增强职责:在明星出场表演之前,经纪人会安排好安保和行程;表演之后,他会处理媒体公关和后续款项。
- 接口一致:对你来说,和经纪人沟通就等同于和明星沟通,经纪人提供了与明星一致的“合作”接口。
代码世界里的代理,就像这位尽职尽责的经纪人。
二、代理模式的 N 种玩法
代理模式的应用非常广泛,根据“经纪人”职责的不同,可以分为多种类型。我们来看几个在 JavaScript 中最常见的例子。
场景一:缓存代理 (Caching Proxy)
这是代理模式最常见的用途之一。对于一些计算成本高昂的操作,我们可以将结果缓存起来,下次再请求时,如果参数相同,就直接返回缓存的结果,而无需再次执行真实操作。
未使用代理的代码:
javascript
// 一个计算开销很大的函数
function expensiveCalculation(arg) {
console.log(`正在进行昂贵的计算,参数: ${arg}...`);
let result = 0;
// 模拟耗时
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result + arg;
}
console.time("第一次计算");
expensiveCalculation(10);
console.timeEnd("第一次计算"); // 耗时很长
console.time("第二次计算");
expensiveCalculation(10); // 同样的参数,耗时依然很长!
console.timeEnd("第二次计算");使用缓存代理重构:
javascript
// 代理函数
const createCachedProxy = (targetFn) => {
const cache = new Map(); // 用 Map 来做缓存
return function (...args) {
const cacheKey = JSON.stringify(args);
if (cache.has(cacheKey)) {
console.log("从缓存中获取结果...");
return cache.get(cacheKey);
} else {
const result = targetFn.apply(this, args);
cache.set(cacheKey, result);
return result;
}
};
};
const cachedCalculation = createCachedProxy(expensiveCalculation);
console.time("第一次计算 (代理)");
cachedCalculation(10);
console.timeEnd("第一次计算 (代理)"); // 耗时很长
console.time("第二次计算 (代理)");
cachedCalculation(10); // 瞬间返回!
console.timeEnd("第二次计算 (代理)");我们的 createCachedProxy 就像一个“经纪人”,它接管了对 expensiveCalculation 的调用,并悄悄地增加了缓存功能。
场景二:虚拟代理 (Virtual Proxy)
虚拟代理用于延迟一个高开销对象的创建或初始化,直到它真正被需要的时候。一个经典的例子是图片懒加载。
未使用代理的代码:
javascript
// 直接设置图片 src,在图片加载完成前,页面会有一段空白
const imgNode = document.getElementById("my-image");
imgNode.src = "https://very-large-image.jpg";使用虚拟代理实现图片预加载:
javascript
const imageProxy = (function () {
const imgNode = document.getElementById("my-image");
return {
setSrc: function (src) {
// 1. 先设置一张本地的 loading 图片作为占位符
imgNode.src = "./loading.gif";
// 2. 创建一个隐藏的 Image 对象,让它在后台加载真实图片
const realImage = new Image();
realImage.src = src;
// 3. 监听加载完成事件
realImage.onload = function () {
// 加载完成后,再将真实图片的 src 赋给页面上的 img 节点
imgNode.src = src;
};
},
};
})();
// 使用代理
imageProxy.setSrc("https://very-large-image.jpg");在这里,imageProxy 作为 imgNode 的代理,它接管了设置 src 的请求,并通过增加“预加载”这个中间步骤,优化了用户体验。
三、ES6 Proxy:语言层面的支持
现代 JavaScript 提供了原生的 Proxy 对象,让创建代理变得前所未有的简单和强大。它允许你使用“陷阱” (traps) 来拦截并重定义一个对象的基本操作(如属性查找、赋值、函数调用等)。
场景:验证代理 (Validation Proxy) 我们希望给一个对象设置属性时,自动进行数据校验。
javascript
// 目标对象
const user = {
name: "张三",
age: 25,
};
// 创建一个代理
const userProxy = new Proxy(user, {
// `set` 是一个“陷阱”,它会拦截所有对 userProxy 的属性赋值操作
set: function (target, key, value) {
if (key === "age") {
if (typeof value !== "number" || value < 0 || value > 100) {
throw new Error("无效的年龄!");
}
}
// 验证通过,才真正设置到原对象上
target[key] = value;
return true; // 表示设置成功
},
});
userProxy.age = 30; // 成功
console.log(user.age); // 30
try {
userProxy.age = 200; // 抛出错误: 无效的年龄!
} catch (e) {
console.error(e.message);
}
console.log(user.age); // 30 (赋值失败,值未改变)ES6 Proxy 的能力远不止于此,它是实现许多现代前端框架(如 Vue 3)响应式系统的核心。
四、与适配器模式的对比
| 对比维度 | 代理模式 (Proxy Pattern) | 适配器模式 (Adapter Pattern) |
|---|---|---|
| 意图 | 控制和管理对一个对象的访问 | 转换一个不兼容的接口 |
| 接口关系 | 代理和真实主体实现相同的接口 | 适配器将一个接口转换成另一个接口 |
| 比喻 | 经纪人 | 电源转接头 |
五、优缺点与适用场景
优点
- 高扩展性:可以在不修改目标对象的前提下,为其增加各种功能(缓存、校验、日志等)。
- 职责分离:代理负责控制逻辑,目标对象负责业务逻辑,符合单一职责原则。
- 安全性:可以作为保护代理,在客户端和目标对象之间建立一道安全屏障。
缺点
- 性能影响:由于在客户端和目标对象之间增加了一层,某些情况下可能会导致请求处理速度变慢。
- 增加复杂性:引入代理对象会增加系统的设计复杂度。
适用场景
- 懒加载/虚拟代理:当一个对象的创建成本很高时。
- 缓存代理:为开销大的操作提供缓存。
- 保护代理:控制对核心对象的访问权限。
- 事件监听与数据绑定:拦截对象的
set和get操作来实现数据响应式。