JavaScript设计模式之代理模式

137 阅读6分钟

代理模式

定义

代理模式是为一个对象提供一个代用品,以便控制对它的访问。

定义上一下子可能难以理解,我们来看看一个表示实际生活中会出现的代码情况。

代理模式的实现

我们先来实现一段不用代理模式的代码。 实现的内容是小明献花给A女神。

var Flower = function () {};
var xiaoming = {
  sendFlower: function (target) {
    var flower = new Flower();
    target.receiveFlower(flower);
  },
};
var A = {
  receiveFlower: function (flower) {
    console.log("收到花 " + flower);
  },
};
xiaoming.sendFlower(A);

接着我们实现一个非常简单的代理模式。 小明把花给朋友B,献花给女神A。

var Flower = function(){};
var xiaoming = {
sendFlower: function( target){
var flower = new Flower();
target.receiveFlower( flower );
}
};
var B = {
  receiveFlower: function (flower) {
    A.receiveFlower(flower);
  },
};
var A = {
  receiveFlower: function (flower) {
    console.log("收到花 " + flower);
  },
};
xiaoming.sendFlower(B);

很显然,执行结果跟第一段代码一致,至此我们就完成了一个最简单的代理模式的编写。也许读者会疑惑,小明自己去送花和代理 B 帮小明送花,二者看起来并没有本质的区别,引入一个代理对象看起来只是把事情搞复杂了而已。

现在我们改变故事的背景设定,假设当 A 在心情好的时候收到花,小明表白成功的几率有60%,而当 A在心情差的时候收到花,小明表白的成功率无限趋近于 0。

小明跟 A 刚刚认识两天,还无法辨别 A 什么时候心情好。如果不合时宜地把花送给 A,花被直接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。

但是 A 的朋友 B 却很了解 A,所以小明只管把花交给 B, B 会监听 A 的心情变化,然后选择 A 心情好的时候把花转交给 A,代码如下:

var Flower = function () {};
var xiaoming = {
  sendFlower: function (target) {
    var flower = new Flower();
    target.receiveFlower(flower);
  },
};
var B = {
  receiveFlower: function (flower) {
    A.listenGoodMood(function () {
      // 监听 A 的好心情
      A.receiveFlower(flower);
    });
  },
};

var A = {
  receiveFlower: function (flower) {
    console.log("收到花 " + flower);
  },
  listenGoodMood: function (fn) {
    setTimeout(function () {
      // 假设 10 秒之后 A 的心情变好
      fn();
    }, 10000);
  },
};
xiaoming.sendFlower(B);

代理模式的分类

代理模式根据作用去分,可以分为

  1. 保护代理:用于对象应该有不同访问权限的情况
  2. 虚拟代理:虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
  3. 缓存代理:缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
  4. 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
  5. 远程代理:为一个对象在不同的地址空间提供局部代表,在 Java 中,远程代理可以是另一个虚拟机中的对象
  6. 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  7. 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体, DLL(操作系统中的动态链接库)是其典型运用场景

js代理模式的实现

在了解了代理模式的意义,我们来直接实现一下常见的两种代理模式,代理模式并没有一个明确的模板可以一直套用,而是一个比较灵活变化的设计模式,它本质上就是为了解耦。

  1. 缓存代理: 我们来编写一个简单的求乘积函数,这个函数式一个纯函数,可以改成任何一个复杂的运算。
var mult = function () {
  console.log("开始计算乘积");
  var a = 1;
  for (var i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
};
mult(2, 3); // 输出: 6
mult(2, 3, 4); // 输出: 24

现在使用代理,去实现一个缓存的功能:

var proxyMult = (function () {
  var cache = {};
  return function () {
    var args = Array.prototype.join.call(arguments, ",");
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = mult.apply(this, arguments));
  };
})();
proxyMult( 1, 2, 3, 4 ); // 输出: 24
proxyMult( 1, 2, 3, 4 ); // 输出: 24

我们只需要专心的去编写函数,而将执行函数交给代理函数即可,它能自动帮我们去利用缓存。

我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。显然这里也可以引入缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。具体代码不再赘述,读者可以自行实现。

另外,这个代理函数,也可以通过用高阶函数动态的创建代理函数。

var createProxyFactory = function (fn) {
  var cache = {};
  return function () {
    var args = Array.prototype.join.call(arguments, ",");
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = fn.apply(this, arguments));
  };
};
var proxyMult = createProxyFactory(mult);

这样一来,我们只需要用这个函数动态创建代理函数就可以实现缓存了。

  1. 虚拟代理: 在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。
var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {
    setSrc: function (src) {
      imgNode.src = src;
    },
  };
})();
myImage.setSrc("http://test.jpg");

我们把网速调至 5KB/s,然后通过 MyImage.setSrc 给该 img 节点设置src,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的菊花图 loading.gif, 来提示用户图片正在加载。代码如下:

var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {
    setSrc: function (src) {
      imgNode.src = src;
    },
  };
})();
var proxyImage = (function () {
  var img = new Image();
  img.onload = function () {
    myImage.setSrc(this.src);
  };
  return {
    setSrc: function (src) {
      myImage.setSrc("./loading.gif");
      img.src = src;
    },
  };
})();
proxyImage.setSrc("http://test.jpg");

我们利用代理去创建一个图片的实例,这样我们可以更加关心myImage这个函数的实现,至于如何实现占位符loading,我们直接交给了proxyImage这个代理函数去做,完成了解耦,以及满足了单一职责原则。

如果不使用代理模式,当然也是可以完成占位符,那么一个函数的里面的内容将会非常多,引起它变化的原因可能会有多个,不利于后续的开发维护,试想一下,如果不这么写,那么当我们要修改loading图片的时候,就要深入一个比较复杂的函数去阅读代码。

总结

代理模式是非常常用的,在JavaScript中最常用的是虚拟代理和缓存代理,但是我们在编写业务代码时也要判断是否真的需要使用代理模式,只有当不方便直接访问对象时,在编写代理更好,毕竟使用代理模式,是一定程度上增加了代码的复杂程度的。