Skip to content

4.2 代理模式 (Proxy Pattern)

代理模式是结构型模式中非常重要且功能强大的一员。它旨在为另一个对象提供一个“替身”或“占位符”,以便控制对这个对象的访问。

核心定义:为目标对象创建一个代理对象,以便客户端通过这个代理对象间接地访问目标对象。这样做的好处是可以在不修改目标对象代码的情况下,增加一些额外的逻辑。

一、生活中的场景:明星与经纪人

  • 明星 (Real Subject / 真实主体):他是真正会唱歌、演戏的核心人物。但他非常繁忙,而且不希望被外界随意打扰。
  • 你想找明星合作 (Client / 客户端):你不能直接联系到明星本人。
  • 经纪人 (Proxy / 代理):你只能联系明星的经纪人。经纪人会作为明星的“代理人”与你沟通。

这个“经纪人”做了什么?

  1. 访问控制:他会帮你过滤掉不合适的请求(比如粉丝的骚扰电话),只把真正重要的工作(比如电影邀约)转达给明星。
  2. 增强职责:在明星出场表演之前,经纪人会安排好安保和行程;表演之后,他会处理媒体公关和后续款项。
  3. 接口一致:对你来说,和经纪人沟通就等同于和明星沟通,经纪人提供了与明星一致的“合作”接口。

代码世界里的代理,就像这位尽职尽责的经纪人。

二、代理模式的 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)
意图控制和管理对一个对象的访问转换一个不兼容的接口
接口关系代理和真实主体实现相同的接口适配器将一个接口转换成另一个接口
比喻经纪人电源转接头

五、优缺点与适用场景

优点

  1. 高扩展性:可以在不修改目标对象的前提下,为其增加各种功能(缓存、校验、日志等)。
  2. 职责分离:代理负责控制逻辑,目标对象负责业务逻辑,符合单一职责原则。
  3. 安全性:可以作为保护代理,在客户端和目标对象之间建立一道安全屏障。

缺点

  1. 性能影响:由于在客户端和目标对象之间增加了一层,某些情况下可能会导致请求处理速度变慢。
  2. 增加复杂性:引入代理对象会增加系统的设计复杂度。

适用场景

  • 懒加载/虚拟代理:当一个对象的创建成本很高时。
  • 缓存代理:为开销大的操作提供缓存。
  • 保护代理:控制对核心对象的访问权限。
  • 事件监听与数据绑定:拦截对象的 setget 操作来实现数据响应式。