代理模式是为一个对象提供一个占位符,以便控制对它的访问。 代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。 代理模式的关键是当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。本文将详细介绍代理模式
比如,实现一个小明让 B 代替自己向 A 送花的功能。首先,不引入代理,小明直接送花给 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,即小明通过 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);
现在改变故事的背景设定,假设当 A 在心情好的时候收到花,小明表白成功的几率有 60%,而当 A 在心情差的时候收到花,小明表白的成功率无限趋近于 0。小明跟 A 刚刚认识两天,还无法辨别 A 什么时候心情好。如果不合时宜地把花送给 A,花被直接扔掉的可能性很大。但是 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);
虽然这只是个虚拟的例子,但可以从中找到两种代理模式的身影
【保护代理】
代理 B 可以帮助 A 过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B 处被拒绝掉。这种代理叫作保护代理
【虚拟代理】
把 newFlower 的操作交给代理 B 去执行,代理 B 会选择在 A 心情好时再执行 newFlower,这是代理模式的另一种形式,叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下:
- var B = {
- receiveFlower: function(flower) {
- A.listenGoodMood(function() { // 监听A 的好心情
- A.receiveFlower(flower); //延迟创建flower对象
- });
- }
- };
在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理
下面来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个 img 标签,并且提供一个对外的 setSrc 接口,外界调用这个接口,便可以给该 img 标签设置
- var myImage = (function() {
- var imgNode = document.createElement('img');
- document.body.appendChild(imgNode);
- return {
- setSrc: function(src) {
- imgNode.src = src;
- }
- }
- })();
- myImage.setSrc('https://static.xiaohuochai.site/icon/icon_200.png');
现在开始引入代理对象 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('https://static.xiaohuochai.site/icon/icon_200.png');
现在通过 proxyImage 间接地访问 myImage。proxyImage 控制了客户对 myImage 的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为一张本地的 loading 图片
如果不使用代理,则图片预加载的函数实现代码如下
- var MyImage = (function() {
- var imgNode = document.createElement('img');
- document.body.appendChild(imgNode);
- var img = new Image;
- img.onload = function() {
- imgNode.src = img.src;
- };
- return {
- setSrc: function(src) {
- imgNode.src = 'loading.gif';
- img.src = src;
- }
- }
- })();
- MyImage.setSrc('https://static.xiaohuochai.site/icon/icon_200.png');
下面引入一个面向对象设计的原则——单一职责原则
单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏
职责被定义为 "引起变化的原因"。上面代码中的 MyImage 对象除了负责给 img 节点设置 src 外,还要负责预加载图片。在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现
另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放——封闭原则。如果只是从网络上获取一些体积很小的图片,或者 5 年后的网速快到根本不再需要预加载,可能希望把预加载图片的这段代码从 MyImage 对象里删掉。这时候就不得不改动 MyImage 对象了
实际上,需要的只是给 img 节点设置 src,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体 MyImage
纵观整个程序,并没有改变或者增加 MyImage 的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放——封闭原则的。给 img 节点设置 src 和图片预加载这两个功能, 被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天不再需要预加载, 那么只需要改成请求本体而不是请求代理对象即可
【代理和本体接口的一致性】
代理对象和本体都对外提供了 setSrc 方法,在客户看来,代理对象和本体是一致的, 代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处:1、用户可以放心地请求代理,只关心是否能得到想要的结果;2、在任何使用本体的地方都可以替换成使用代理
如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都 能被执行,则可以认为它们也具有一致的 "接口"
- var myImage = (function() {
- var imgNode = document.createElement('img');
- document.body.appendChild(imgNode);
- return function(src) {
- imgNode.src = src;
- }
- })();
- var proxyImage = (function() {
- var img = new Image;
- img.onload = function() {
- myImage(this.src);
- }
- return function(src) {
- myImage('file:// /C:/Users/svenzeng/Desktop/loading.gif');
- img.src = src;
- }
- })();
- proxyImage('https://static.xiaohuochai.site/icon/icon_200.png');
在 Web 开发中,也许最大的开销就是网络请求。假设在做一个文件同步的功能,选中一个 checkbox 时,它对应的文件就会被同步到另外一台备用服务器上面
首先,在页面中放置好这些 checkbox 节点
- type="checkbox" id="1">1
- type="checkbox" id="2">2
- type="checkbox" id="3">3
- type="checkbox" id="4">4
- type="checkbox" id="5">5
- type="checkbox" id="6">6
- type="checkbox" id="7">7
- type="checkbox" id="8">8
- type="checkbox" id="9">9
接下来,给这些 checkbox 绑定点击事件,并且在点击的同时往另一台服务器同步文件
- var synchronousFile = function(id) {
- console.log('开始同步文件,id 为: ' + id);
- };
- var checkbox = document.getElementsByTagName('input');
- for (var i = 0,
- c; c = checkbox[i++];) {
- c.onclick = function() {
- if (this.checked === true) {
- synchronousFile(this.id);
- }
- }
- };
当选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。可以预见,如此频繁的网络请求将会带来相当大的开销。解决方案是可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力
- var synchronousFile = function(id) {
- console.log('开始同步文件,id 为: ' + id);
- };
- var proxySynchronousFile = (function() {
- var cache = [],
- // 保存一段时间内需要同步的ID
- timer; // 定时器
- return function(id) {
- cache.push(id);
- if (timer) { // 保证不会覆盖已经启动的定时器
- return;
- }
- timer = setTimeout(function() {
- synchronousFile(cache.join(',')); // 2 秒后向本体发送需要同步的ID 集合
- clearTimeout(timer); // 清空定时器
- timer = null;
- cache.length = 0; // 清空ID 集合
- },
- 2000);
- }
- })();
- var checkbox = document.getElementsByTagName('input');
- for (var i = 0,
- c; c = checkbox[i++];) {
- c.onclick = function() {
- if (this.checked === true) {
- proxySynchronousFile(this.id);
- }
- }
- };
假设要加载 miniConsole.js 这个文件,该文件用于打印 log,但该文件很大。所以,通常解决方案是用一个占位的 miniConsole 代理对象来给用户提前使用,然后将打印 log 的请求都包裹在一个函数里,随后这些函数将全部放到缓存队列中,这些逻辑都是在 miniConsole 代理对象中完成实现的。等用户按下 F2 唤出控制台的时候,才开始加载真正的 miniConsole.js 的代码,加载完成之后将遍历 miniConsole 代理对象中的缓存函数队列,同时依次执行它们
未加载真正的 miniConsole.js 之前的代码如下:
- var cache = [];
- var miniConsole = {
- log: function() {
- var args = arguments;
- cache.push(function() {
- return miniConsole.log.apply(miniConsole, args);
- });
- }
- };
- miniConsole.log(1);
当用户按下 F2 时,开始加载真正的 miniConsole.js,代码如下:
- var handler = function(ev) {
- if (ev.keyCode === 113) {
- var script = document.createElement('script');
- script.onload = function() {
- for (var i = 0,
- fn; fn = cache[i++];) {
- fn();
- }
- };
- script.src = 'miniConsole.js';
- document.getElementsByTagName('head')[0].appendChild(script);
- }
- };
- document.body.addEventListener('keydown', handler, false);
- // miniConsole.js 代码:
- miniConsole = {
- log: function() {
- // 真正代码略
- console.log(Array.prototype.join.call(arguments));
- }
- };
要注意的是,要保证在 F2 被重复按下时,miniConsole.js 只被加载一次
- var miniConsole = (function() {
- var cache = [];
- var handler = function(ev) {
- if (ev.keyCode === 113) {
- var script = document.createElement('script');
- script.onload = function() {
- for (var i = 0,
- fn; fn = cache[i++];) {
- fn();
- }
- };
- script.src = 'miniConsole.js';
- document.getElementsByTagName('head')[0].appendChild(script);
- document.body.removeEventListener('keydown', handler); // 只加载一次miniConsole.js
- }
- };
- document.body.addEventListener('keydown', handler, false);
- return {
- log: function() {
- var args = arguments;
- cache.push(function() {
- return miniConsole.log.apply(miniConsole, args);
- });
- }
- }
- })();
- miniConsole.log(11); // 开始打印log
- // miniConsole.js 代码
- miniConsole = {
- log: function() {
- // 真正代码略
- console.log(Array.prototype.join.call(arguments));
- }
- }
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
下面是一个计算乘积的例子
先创建一个用于求乘积的函数:
- 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
当第二次调用 proxyMult(1,2,3,4) 的时候,本体 mult 函数并没有被计算,proxyMult 直接返回了之前缓存好的计算结果。通过增加缓存代理的方式,mult 函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的
在项目中常常遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。显然这里也可以引入缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式
通过传入高阶函数的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建缓存代理的工厂中,这样一来,就可以为乘法、加法、减法等创建缓存代理,代码如下:
- /**************** 计算乘积 *****************/
- var mult = function() {
- var a = 1;
- for (var i = 0,
- l = arguments.length; i < l; i++) {
- a = a * arguments[i];
- }
- return a;
- };
- /**************** 计算加和 *****************/
- var plus = function() {
- var a = 0;
- for (var i = 0,
- l = arguments.length; i < l; i++) {
- a = a + arguments[i];
- }
- return a;
- };
- /**************** 创建缓存代理的工厂 *****************/
- 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),
- proxyPlus = createProxyFactory(plus);
- alert(proxyMult(1, 2, 3, 4)); // 输出:24
- alert(proxyMult(1, 2, 3, 4)); // 输出:24
- alert(proxyPlus(1, 2, 3, 4)); // 输出:10
- alert(proxyPlus(1, 2, 3, 4)); // 输出:10
代理模式的变体种类非常多,还包括以下几种
1、防火墙代理:控制网络资源的访问,保护主题不让 "坏人" 接近
2、远程代理:为一个对象在不同的地址空间提供局部代表
3、保护代理:用于对象应该有不同访问权限的情况
4、智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数
5、写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景
代理模式包括许多小分类,在 javascript 开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但在编写业务代码时,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟
来源: http://www.cnblogs.com/xiaohuochai/p/8030653.html