代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
基本可以理解为粉丝(客户),经纪人(代理),偶像(对象)。经纪人就相当于偶像的代理,需求直接提给经纪人,经纪人这边可以进行很多逻辑上的处理,比如可以帮助偶像过滤掉很多请求等等。
1.保护代理和虚拟代理
像上面那种,请求被代理拒绝掉就是保护代理。
把一些开销很大的对象,延迟到真正需要它的时候才去创建的代理,就是虚拟代理。
保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。
2.虚拟代理实现图片预加载
在Web 开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。
先随便创建一个img对象
1 var myImage = (function(){ 2 var imgNode = document.createElement( ‘img‘ ); //创建节点 3 document.body.appendChild( imgNode ); 4 return { //闭包返回一个对象,包含可以设置节点src属性的方法 5 setSrc: function( src ){ 6 imgNode.src = src; 7 } 8 } 9 })(); 10 11 myImage.setSrc( ‘真正的图片.jpg‘ );
这样的代码在网速很慢时,图片位置会出现较长时间空白。现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位图loading.gif,来提示用户图片正在加载。
1 var myImage = (function(){ 2 var imgNode = document.createElement( ‘img‘ ); //同上 3 document.body.appendChild( imgNode ); 4 return { 5 setSrc: function( src ){ 6 imgNode.src = src; 7 } 8 } 9 })(); 10 11 var proxyImage = (function(){ 12 var img = new Image; //创建一个图片对象 13 img.onload = function(){ //图片对象异步加载完成后,加载完成包含图片的加载完成,这句代码可以理解为一个监控 14 myImage.setSrc( this.src ); //把节点的src设置成图片对象的 15 } 16 return { 17 setSrc: function( src ){ 18 myImage.setSrc( ‘loading.gif‘ ); //预先给个占位图片 19 img.src = src; //给图片对象设置src属性 20 } 21 } 22 })(); 23 24 proxyImage.setSrc( ‘真正的图片.jpg‘ ); //调用这个方法,会先给目标节点一个占位图片,同时内置的图片对象开始加载图片,当加载完成后,触发设置,节点显示正确图片
这里我们也可以看出,资源的加载只需要一次,只要这张图片下载好了,通过src都能很快显示它。
3.代理的意义
图片预加载功能不通过代理也可以实现,如下:
1 var MyImage = (function(){ 2 var imgNode = document.createElement( ‘img‘ ); //创建节点 3 document.body.appendChild( imgNode ); 4 var img = new Image; //创建一个图片对象 5 img.onload = function(){ //图片对象异步加载 6 imgNode.src = img.src; //节点更换src 7 }; 8 return { 9 setSrc: function( src ){ 10 imgNode.src = ‘loading.gif‘; //预先给个占位图片 11 img.src = src; //图片对象设置src 12 } 13 } 14 })(); 15 16 MyImage.setSrc( ‘真正的图片.jpg‘ );
看起来没什么问题,但是我们得提到一个面向对象设计的原则——单一职责原则。
一个类(通常也包括对象和函数等),应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏,我们可能不得不繁琐的修改代码或者重写函数。
比如上段代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
我们需要的只是给img 节点设置src,预加载图片只是一个锦上添花的功能。
使用代理模式的代码中,我们并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放—封闭原则的。给img节点设置src和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。
4.代理和本体接口的一致性
如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc 方法。这样做有两个好处,(1)使用者不会被api搞糊涂(2)在任何使用本体的地方都可以替换成使用代理。
特别:如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”。
1 var myImage = (function(){ 2 var imgNode = document.createElement( ‘img‘ ); 3 document.body.appendChild( imgNode ); 4 return function( src ){ 5 imgNode.src = src; 6 } 7 })(); 8 9 var proxyImage = (function(){ 10 var img = new Image; 11 img.onload = function(){ 12 myImage( this.src ); 13 } 14 return function( src ){ 15 myImage( ‘loading.gif‘ ); 16 img.src = src; 17 } 18 })(); 19 20 proxyImage( ‘真正的图片.jpg‘ );
5.虚拟代理合并HTTP请求
前面高阶函数的时候提过节流函数,这里也是一样,为了避免可能频繁触发的请求导致的服务器压力,可以设置一个时间延迟,比如说2s,2s后会把请求一起提交一次。只不过现在这里通过代理来实施。
6.缓存代理
缓存代理的例子——计算乘积
1 var mult = function(){ //计算乘积的函数 2 console.log( ‘开始计算乘积‘ ); 3 var a = 1; 4 for ( var i = 0, l = arguments.length; i < l; i++ ){ //遍历参数,计算结果 5 a = a * arguments[i]; 6 } 7 return a; //返回结果 8 }; 9 10 mult( 2, 3 ); // 输出:6 11 mult( 2, 3, 4 ); // 输出:24 12 13 var proxyMult = (function(){ 14 var cache = {}; //利用闭包 保留结果,作为缓存 15 return function(){ 16 var args = Array.prototype.join.call( arguments, ‘,‘ ); //把参数变成字符串 17 if ( args in cache ){ //如果这个字符串名称在缓存中存在 18 return cache[ args ]; //返回结果 19 } 20 return cache[ args ] = mult.apply( this, arguments ); //不存在,就调用乘积函数计算一次,保存结果 21 } 22 })(); 23 24 proxyMult( 1, 2, 3, 4 ); // 输出:24 25 proxyMult( 1, 2, 3, 4 ); // 输出:24 第二次调用,没有再次计算,而是拿的缓存中的结果
用高阶函数动态创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。
1 /**************** 计算乘积 *****************/ 2 var mult = function(){ 3 var a = 1; 4 for ( var i = 0, l = arguments.length; i < l; i++ ){ 5 a = a * arguments[i]; 6 } 7 return a; 8 }; 9 /**************** 计算加和 *****************/ 10 var plus = function(){ 11 var a = 0; 12 for ( var i = 0, l = arguments.length; i < l; i++ ){ 13 a = a + arguments[i]; 14 } 15 return a; 16 }; 17 /**************** 创建缓存代理的工厂 *****************/ 18 var createProxyFactory = function( fn ){ //这里只是把最后调用的函数变成参数传进来而已 19 var cache = {}; 20 return function(){ 21 var args = Array.prototype.join.call( arguments, ‘,‘ ); 22 if ( args in cache ){ 23 return cache[ args ]; 24 } 25 return cache[ args ] = fn.apply( this, arguments ); 26 } 27 }; 28 29 var proxyMult = createProxyFactory( mult ), 30 proxyPlus = createProxyFactory( plus ); 31 alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24 32 alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24 33 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10 34 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
总结
代理模式包括许多小分类,在JavaScript 开发中最常用的是虚拟代理和缓存代理。代理模式可以理解为一个中转站,其内部必然会对本体进行调用。我们在编写业务代码的时候,不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。