代理模式:
一个对象不能直接引用另一个对象,需要代理对象在这两个对象中起到中介的作用。
1.代理的例子——小明追小红
小明决定给小红送一束花来表白,小明决定让小红的闺蜜小兰来代替自己完成送花这件事情。
先看看不用代理模式的情况:
var Flower = function () { };
var ming = {
sendFlower: function (target) {
var flower = new Flower();
target.receiveFlower(flower);
}
};
var hong = {
receiveFlower: function (flower) {
console.log('收到花 ' + flower);
}
};
ming.sendFlower(hong);
接下来,我们引入代理 小兰,即小明通过 小兰 来给 小红送花:
var Flower = function () { };
var ming = {
sendFlower: function (target) {
var flower = new Flower();
target.receiveFlower(flower);
}
};
var lan = {
receiveFlower: function (flower) {
hong.receiveFlower(flower);
}
};
var hong = {
receiveFlower: function (flower) {
console.log('收到花 ' + flower);
}
};
ming.sendFlower(lan);
也许读者会疑惑,小明自己去送花和代理 小兰帮小明送花,二者看起来并没有本质的区别,引 入一个代理对象看起来只是把事情搞复杂了而已。
可是其实小兰起到了很重要的作用,假设当小红在心情好的时候收到花,小明表白成功的几率有 60%,而当小红在心情差的时候收到花,小明表白的成功率无限趋近于 0。
作为闺蜜的小兰就可以在合适的时间送花,来提高成功的概率了。
代码如下:
var Flower = function(){};
var ming = {
sendFlower: function( target){
var flower = new Flower();
target.receiveFlower( flower ); }
};
var lan = {
receiveFlower: function( flower ){
hong.listenGoodMood(function(){ // 监听 小红 的好心情
hong.receiveFlower( flower );
});
}
};
var hong = {
receiveFlower: function( flower ){
console.log( '收到花 ' + flower );
},
listenGoodMood: function( fn ){
setTimeout(function(){ // 假设 10 秒之后 小红 的心情变好
fn();
}, 10000 );
}
};
xiaoming.sendFlower( lan );
2 保护代理和虚拟代理
虽然上面的只是个虚拟的例子,但我们可以从中找到两种代理模式的身影。
保护代理:
代理小兰可以帮助小红过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 小兰 处被拒绝掉。这种代理叫作保护代理。
虚拟代理:
假设现实中的花价格不菲,导致在程序世界里,new Flower 也是一个代价昂贵的操作, 那么我们可以把 new Flower 的操作交给代理 小兰 去执行,代理 小兰 会选择在 小红心情好时再执行 new Flower,这是代理模式的另一种形式,叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
var lan = {
receiveFlower: function( flower ){
hong.listenGoodMood(function(){ // 监听 小红 的好心情
var flower = new Flower(); // 延迟创建 flower 对象
hong.receiveFlower( flower );
});
}
};
保护代理用于控制不同权限的对象对目标对象的访问,但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式,我们主要讨论的也是虚拟代理。
3. 虚拟代理实现图片预加载
在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性, 由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种 场景就很适合使用虚拟代理。
我们来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建 一个 img 标签,并且提供一个对外的 setSrc 接口,外界调用这个接口,便可以给该 img 标签设置src 属性:
var myImage = (function(){
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
setSrc: function( src ){
imgNode.src = src; }
}
})();
myImage.setSrc( 'https://www.baidu.com/img/bd_logo1.png' );
我们把网速调至 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( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
现在我们通过 proxyImage 间接地访问 MyImage。proxyImage 控制了客户对 MyImage 的访问,并 且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为 一张本地的 loading 图片。
4. 代理的意义
也许会有疑问,不过是实现一个小小的图片预加载功能,即使不需要引入任何模式也能 办到,那么引入代理模式的好处究竟在哪里呢?下面我们先抛开代理,编写一个更常见的图片预加载函数。
不用代理的预加载图片函数实现如下:
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 = 'file:// /C:/Users/svenzeng/Desktop/loading.gif';
img.src = src;
}
}
})();
MyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
我们讲过单一责任原则,简而言之就是一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。
如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可 能会有多个。
面向对象设计鼓励将行为分布到不同的对象之中,如果一个对象承担的职责过多, 等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
职责被定义为“引起变化的原因”。上段代码中的 MyImage 对象除了负责给 img 节点设置 src 外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职 责的实现。
另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放— 封闭原则。如果我们只是从网络上获取一些体积很小的图片,或者 5 年后的网速快到根本不再需 要预加载,我们可能希望把预加载图片的这段代码从 MyImage 对象里删掉。这时候就不得不改动 MyImage 对象了。
实际上,我们需要的只是给 img 节点设置 src,预加载图片只是一个锦上添花的功能。如果 能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出 来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体 MyImage。
纵观整个程序,我们并没有改变或者增加 MyImage 的接口,但是通过代理对象,实际上给系 统添加了新的行为。这是符合开放—封闭原则的。给 img 节点设置 src 和图片预加载这两个功能, 被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载, 那么只需要改成请求本体而不是请求代理对象即可。
5. 代理和本体接口的一致性
在上面的代码中,我们的代理对象和本体都对外提供了 setSrc 方法,在客户看来,代理对象和本体是一致的, 代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处。
- 用户可以放心地请求代理,他只关心是否能得到想要的结果。
- 在任何使用本体的地方都可以替换成使用代理。
6. 虚拟代理合并http 请求
先想象这样一个场景:每周我们都要写一份工作周报,周报要交给总监批阅。总监手下管理 着 150 个员工,如果我们每个人直接把周报发给总监,那总监可能要把一整周的时间都花在查看 邮件上面。
现在我们把周报发给各自的组长,组长作为代理,把组内成员的周报合并提炼成一份后一次 4 性地发给总监。这样一来,总监的邮箱便清净多了。
这个例子在程序世界里很容易引起共鸣,在 Web 开发中,也许最大的开销就是网络请求。 假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同 步到另外一台备用服务器上面,我们先在页面中放置好这些 checkbox 节点:
<body>
<input type="checkbox" id="1"></input>1
<input type="checkbox" id="2"></input>2
<input type="checkbox" id="3"></input>3
<input type="checkbox" id="4"></input>4
<input type="checkbox" id="5"></input>5
<input type="checkbox" id="6"></input>6
<input type="checkbox" id="7"></input>7
<input type="checkbox" id="8"></input>8
<input type="checkbox" id="9"></input>9
</body>
接下来,给这些 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 次同步文件的请求。而点击一个 checkbox 并不是很复杂的操作,如果我们点的非常得快,可以预见,如此频繁的网络请求将会带来相当大的开销。
解决方案是,我们可以通过一个代理函数 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 );
}
}
};
7.缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参
数跟之前一致,则可以直接返回前面存储的运算结果。
7.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
当我们第二次调用 proxyMult( 1, 2, 3, 4 )
的时候,本体 mult
函数并没有被计算,proxyMult
直接返回了之前缓存好的计算结果。
通过增加缓存代理的方式,mult
函数可以继续专注于自身的职责——计算乘积,缓存的功能 是由代理对象实现的。
8. 动态创建代理
/**************** 创建缓存代理的工厂 *****************/
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 plus = function(){
var a = 0;
for ( var i = 0, l = arguments.length; i < l; i++ ){ + arguments[i];){
a = a + arguments[i];
}
return a;
};
var proxyPlus = createProxyFactory( plus );
alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
9.其他代理方式
- 防火墙代理: 控制网络资源的访问,保护主题不让“坏人”接近;
- 远程代理: 为一个对象在不同的地址空间提供局部代表,在 Java 中,远程代理可以是另
一个虚拟机中的对象。 - 保护代理: 用于对象应该有不同访问权限的情况。
- 智能引用代理: 取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个
对象被引用的次数。 - 写时复制代理: 通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,
当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL (操作系统中的动态链接库)是其典型运用场景。
参考资料
JavaScript设计模式与开发实践----曾探
JavaScript设计模式----张容铭