『面试的底气』—— 设计模式之代理模式

定义

代码模式:为一个对象提供一个代用品或占位符,以便控制对它的访问。

关键

代理模式的关键是,当程序不方便或者不满足需要去直接访问一个对象的时候,提供一个替身 对象来控制对这个对象的访问,程序实际上访问的是替身对象。替身对象对请求做出一些处理之 后,再把请求转交给本体对象。

JavaScript中的代理模式

代理模式的变体种类非常多,不一定都适合在JavaScript中使用,本文挑两种在JavaScript开发中最常用的两种代理来介绍,分别是虚拟代理和缓存代理。

虚拟代理

虚拟代理会把一些开销很大的对象,延迟到真正需要它的时候才去创建。这里用实现图片预加载功能的例子来介绍虚拟代理。

图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。

先创建一个MyImage类,用来生成一个 img 节点并将插入 body 中。

class MyImage {
  constructor() {
    this.img = document.createElement('img');
    document.body.appendChild(this.img);
  }
  setSrc(src) {
    this.img.src = src;
  }
}

执行const myImage = new MyImage()生成一个 img 节点并将插入 body 中,先在开发者工具中把网速调至Slow 3G,然后通过 执行myImage.setSrc('xxx')给该 img 节点设置 src 属性,可以看到,在图片被加载好之前,页面中有段时间会是一片空白。

为了解决这个问题。写一个代理类ProxyImage,通过这个代理类,实现在图片被真正加载好之前,页面中将出现一张 loading 图片占位, 来提示用户图片正在加载。

class ProxyImage {
  constructor() {
    this.myImage = new MyImage();
    this.image = new Image();
    this.image.onload = function () {
      this.myImage.setSrc(this.image.src);
    }
  }
  setSrc(src) {
    this.myImage.setSrc('./loading.gif');
    this.image.src = src;
  }
}
const proxyImage = new ProxyImage();
proxyImage.setSrc(xxxx);

通过ProxyImage代理类间接地访问MyImage类实例化生成的对象,ProxyImage类控制了对MyImage类实例化生成的对象访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为一张本地的 loading 图片。

执行proxyImage.setSrc(xxxx)后,会先执行this.myImage.setSrc('./loading.gif')给 img 节点的src 属性设置一张 loading 占位图的地址,同时把真正要展示的图片地址 xxxx 赋值给new Image()创建的图片对象的 src 属性,在图片对象加载成功后再把图片地址 xxxx 通过this.myImage.setSrc(this.image.src)重新赋值给 img 节点的 src 属性。

这样在真正要展示的图片加载成功之前,由MyImage类实例化生成的 img 节点会一直展示 loading 的占位图,实现图片预加载的功能。

代理的意义

假如不用虚拟代理可以实现图片预加载的功能吗,当然可以,下面来实现一下:

class MyImage {
  constructor() {
    this.img = document.createElement('img');
    document.body.appendChild(this.img);
    this.image = new Image();
    image.onload = function () {
      this.img.src = this.image.src;
    };
  }
  setSrc(src) {
    this.img.src = './loading.gif';
    this.image.src = src;
  }
}

咋看一下代码量还更少,代码更简单了。但是这么写违背了设计模式的单一职责原则,在MyImage类中除了负责生成 img 节点并设置src,还要负责预加载图片。

假如有一张图片非常小,根本不需要预加载,使用预加载还会出现占位图一闪而过的问题。那怎么办呢?难道要重新写个MyImage类,这会违背开放—封闭原则

实际上MyImage类的功能只需要给 img 节点设置 src,预加载图片只是一个锦上添花的功能。假如把这个功能放在代理中,那么代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,重新交给MyImage类中来给 img 节点设置 src 。

这里并没有改变或者增加 MyImage 的功能,只是通过代理,给MyImage类添加了新的功能。这是符合开放—封闭原则

给 img 节点设置 src 和图片预加载这两个功能,被隔离在两个类里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。 用类来实现代理模式中的虚拟代理,而不使用类的方式也可以实现代理模式,比如用高阶函数也可以实现一个代理模式。接下来用高阶函数来动态创建一个缓存代理来再次介绍代理模式。

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参 数跟之前一致,则可以直接返回前面存储的运算结果。

高阶函数简单的可以理解为把一个函数作为参数传入一个函数中。那么如何用高阶函数动态创建一个缓存代理,就是实现一个专门用于创建缓存代理的工厂函数,把要代理的函数传入这个工厂函数,返回一个代理函数。

const createProxyFactory = (fn) =>{
  let cache = {};
  return function () {
    const args = Array.prototype.join.call(arguments, ',');
    if (args in cache) {
      return cache[args];
    }
    return cache[args] = fn.apply(this, arguments);
  }
};

比如现在要为一个计算乘积的mult函数实现缓存代理,

const mult = function () {
  let a = 1;
  for (let i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
};

mult函数当作参数传入createProxyFactory这个创建缓存代理的函数中即可为mult函数实现缓存代理。

小结

代理模式可以符合单一职责原则,使被代理函数的职责单一,使被代理函数高内聚。可以满足开放-封闭原则,使得被代理函数封闭,对其扩展新功能在代理函数中实现,使被代理函数低耦合。

虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。 当真正发现不方便直接访问某个对象或改变某个函数的内部代码时,再编写代理也不迟。

『面试的底气』—— 设计模式之代理模式的相似文章

『面试的底气』—— 详细说明订阅-发布模式在下面场景的应用,设计模式之发布-订阅模式分析『面试的底气』—— 设计模式遵循的原则,设计模式之单一职责原则分析『面试的底气』—— 设计模式之策略模式分析