代理模式
当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
1. 小明送书
小明送书给大明,先看看不用代理模式如何实现
var Shu = function () { };
var xiaoming = {
sonshu: function (target) {
var shu = new Shu;
return target.shoudao(shu);
}
};
var daming = {
shoudao: function (shu) {
console.log("收到书" + shu);
}
};
xiaoming.sonshu(daming);
小明有事,他让中明去送,这次用代理模式实现
var Shu = function () { };
var xiaoming = {
sonshu: function (target) {
var shu = new Shu;
target.shoudao(shu);
}
};
var zhongming = {
shoudao: function (shu) {
daming.shoudao(shu);
}
}
var daming = {
shoudao: function (shu) {
console.log("收到书" + shu);
}
};
xiaoming.sonshu(zhongming);
大明不在家,中明等他回家再送给他
var Shu = function () { };
var xiaoming = {
sonshu: function (target) {
var shu = new Shu;
target.shoudao(shu);
}
};
var zhongming = {
shoudao: function (shu) {
daming.huijia(function () {
daming.shoudao(shu);
});
}
};
var daming = {
shoudao: function (shu) {
alert("收到书" + shu);
},
huijia: function (fn) {
setTimeout(function () { fn();}, 2000);//假如大明2秒后回家
}
};
xiaoming.sonshu(zhongming);
2. 保护代理和虚拟代理
虽然这只是个虚拟的例子,但我们可以从中找到两种代理模式的身影。代理(中明)可以帮助大明过滤掉一些请求,比如小明拿皇叔给大明,这种请求就可以直接在代理处给拒绝。这种代理叫做保护代理。
另外,假如小明没钱买书,或者在程序中,买书(new Shu)的代价很大,那么我们可以把 new Shu 的操作交给代理去执行,代理会选择在大明回家的时候 new Shu 送给他,这是代理模式的另一种形式,叫做虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候采取创建。代码如下:
var zhongming = {
shoudao: function () {
daming.huijia(function () {
var shu = new Shu;
daming.shoudao(shu);
});
}
};
保护代理用于控制不同权限的对象对目标对象的访问,但在JS中并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式,我们主要讨论的也是虚拟代理,接下了我们来看一个虚拟代理的示例
3. 虚拟代理实现图片预加载
在Web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张小图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到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://img.alicdn.com/imgextra/i1/2406102577/TB2QCq1fVXXXXXvXXXXXXXXXXXX_!!2406102577.jpg');
如果你网速够慢的话,加载这张图片是需要一段时间的。现在我们引入代理对象ProxyImage,通过这个代理对象,在图片真正被加载好之前,页面将出现一张用来占位的小图片,来提示用户图片正在加载。代码如下:
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;
//当大图加载好后再次调用函数改变myImage.src的值
img.onload = function () {
myImage.setSrc(img.src);
};
return {
setSrc: function (src) {
//占位图
myImage.setSrc('https://img.alicdn.com/imgextra/i4/2406102577/TB2u3mOfVXXXXb6XXXXXXXXXXXX_!!2406102577.jpg');
//把大图让给img慢慢加载
img.src = src;
}
}
})();
proxyImage.setSrc('https://img.alicdn.com/imgextra/i1/2406102577/TB2QCq1fVXXXXXvXXXXXXXXXXXX_!!2406102577.jpg');
现在我们通过proxyImage间接的访问myImage。proxyImage控制了客户对myImage的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把imgNode.src设置为一张小图片。
4. 缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
这里演示一个简单的例子——计算阶乘。
先创建一个用于求阶乘的函数:
var mult = function () {
var a = 1;
for (i = 0,l = arguments.length; i < l; i++){
a = a * arguments[i];
}
return a;
};
再加入缓存代理:
var mult = function () {
var a = 1;
for (i = 0,l = arguments.length; i < l; i++){
a = a * arguments[i];
}
return a;
};
var proxyMult = (function () {
var cache = {};
return function () {
//调用数组原型的join方法,将arguments转化为字符串
var args = Array.prototype.join.call(arguments, ',');
//遍历cache对象,如果有其属性则直接返回值
for (args in cache) {
return cache[args];
}
//没有,则计算数值并赋给cache对象
return cache[args] = mult.apply(this, arguments);
};
})();
proxyMult(1, 2, 3, 4);//输出:24
proxyMult(1, 2, 3, 4);//输出:24
当我们第二次调用proxyMult的时候,本体mult函数并没有被计算,proxyMult直接返回了之前缓存好的计算结果。
5. 用高阶函数动态创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为乘法,加法,减法等创建缓存代理,代码如下:
//阶乘函数
var mult = function () {
var a = 1;
for (i = 0,l = arguments.length; i < l; i++){
a = a * arguments[i];
}
return a;
};
//加法函数
var plus = function () {
var a = 0;
for (i = 0, l = arguments.length; i < l; i++) {
a = a + arguments[i];
}
return a;
};
//创建缓存代理工厂
var createProxyFctory = function (fn) {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, ',');
for (args in cache) {
return cache[args];
}
return cache[args] = fn.apply(this, arguments);
};
};
var proxyMult = createProxyFctory(mult);
var proxyPlus = createProxyFctory(plus);
console.log(proxyMult(1, 2, 3, 4));//输出:24
console.log(proxyMult(1, 2, 3, 4));//输出:24
console.log(proxyPlus(1, 2, 3, 4));//输出:10
console.log(proxyPlus(1, 2, 3, 4));//输出:10
6. 小结
代理模式还包括许多小分类,但在JavaScript开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式,当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。
参考书目:《JavaScript设计模式与开发实践》