代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。javascript
在某些状况下,直接访问对象不方便或者对访问对象加强一些功能,可使用到代理模式。好比想请一个明星来办一场商业演出,通常都是联系明星的经纪人,那么经纪人就是明星的代理。前端
在这个故事中,假设妹子是girl对象,小明想要给妹子送花。因为妹子只有一个,就直接经过一个对象字面量表示。java
class Gift {}
class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
sendGift(target) {
const gift = new Gift();
target.receiveGift(this, gift);
}
}
const girl = {
receiveGift(sender, gift) {
console.log(`from ${sender.name}`, sender, gift);
}
}
const xiaoming = new Person('小明', '程序员');
xiaoming.sendGift(girl); // from 小明 Person {name: "小明", job: "程序员"} Gift {}
复制代码
如今妹子收到礼物了,也知道了小明的姓名和工做。但是追求妹子的人不少,妹子一我的收不过来啊,这时候妹子就须要一个代理对象了,称为proxyGirl。react
class Gift {}
class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
sendGift(target) {
const gift = new Gift();
target.receiveGift(this, gift);
}
}
const proxyGirl = {
receiveGift(...args) {
girl.receiveGift(...args);
}
}
const girl = {
receiveGift(sender, gift) {
console.log(`from ${sender.name}`, sender, gift);
}
}
const xiaoming = new Person('小明', '程序员');
xiaoming.sendGift(proxyGirl);
复制代码
这里结果和上述同样,所作的就是增长了一个代理对象。这必然会增长一些代码,增长程序的复杂度。它的好处在于能够经过代理对象,去控制对目标对象的直接访问(见定义)。程序员
好比在proxyGirl中去进行一些过滤。ajax
const proxyGirl = {
receiveGift(...args) {
const sender = args[0];
if(sender.job !== '程序员') {
girl.receiveGift(...args);
} else {
throw sender;
}
}
}
复制代码
若是给妹子送礼物的是程序员,那么把他扔出去。缓存
从上述例子中,能够看到两种代理方式的影子。代理对象能够帮目标对象过滤掉一些请求,好比职业是程序员的,或者没房没车的。这种代理叫作保护代理。bash
另外,假设礼物价值不菲,在程序中new Gift也是一个代价昂贵的操做。那么咱们能够把这个操做交给代理类去执行。代理类首先过滤掉不符合条件的人,而后去new Gift,这是代理类的另外一种形式,叫作虚拟代理,也叫作动态代理。虚拟代理把一些开销很大的对象,延迟到真正须要它的时候才去建立(相似于单例模式中的惰性单例)。服务器
const proxyGirl = {
receiveGift(sender) {
const sender = args[0];
if(sender.job !== '程序员') {
const gift = new Gift();
girl.receiveGift(sender, gift); // 不改变目标对象的参数
} else {
throw sender;
}
}
}
复制代码
前端开发中,直接给img设置目标src不是一个好的作法。当图片体积比较大的时候,不能第一时间显示出来,就会形成空白,这很显然不是一个好的体验。常见的作法是给图片预先设置一个loading图(或分辨率较低的原图),而后用异步的方式加载图片,加载好后再替换原图片的url。这种场景就很适合时候虚拟代理(给目标对象增长loading功能)。网络
const myImage = {
setSrc: (ele, src) => {
ele.src = src;
}
}
const proxyImage = {
checkEle: ele => {
if(ele.tagName !== 'IMG') {
throw '这个对象只能代理img标签';
}
},
setSrc: (ele, src) => {
// 初始设置为loading图片
this.checkEle();
// 设置loading
ele.src = 'loading.png';
// 图片下载好了以后替换原图的url
const img = new Image();
img.src = src;
img.onload = () => {
myImage.setSrc(ele, src);
}
}
}
const img = document.querySelector('.some-img');
proxyImage.setSrc(img, 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1547377781665&di=6f7dd28462a295f04da213e190728681&imgtype=0&src=http%3A%2F%2Fb.zol-img.com.cn%2Fdesk%2Fbizhi%2Fstart%2F2%2F1363857521405.jpg')
复制代码
咱们经过代理对象proxyImage间接访问目标对象myImage,并添加过滤标签功能,增长loading功能。
上面的实现咱们彻底能够放在myImage对象中。
const myImage = {
setSrc: (ele, src) => {
if(ele.tagName !== 'IMG') {
throw '这个对象只能代理img标签';
}
// 设置loading
ele.src = 'loading.png';
// 图片下载好了以后替换原图的url
const img = new Image();
img.src = src;
img.onload = () => {
ele.src = src;
}
}
}
复制代码
好像也没有什么问题。代码确实能正常工做,并达到了预期的效果。不过它违反了单一职责原则。职责被定义为“引发变化的缘由”,就是说有且只有一个缘由引发对象的变化。若是多个缘由都能引发对象变化,那么说明这个对象承担了过多的职责,它将变得巨大,而且职责之间相互耦合,那么必将致使高耦合低内聚的设计。咱们在处理其中一个职责时,有可能由于强耦合性影响到另外一个职责的实现。这对于测试来讲也是很是不便的。
另外,在面向对象的设计中,大多数状况下,若是违反其余任何原则,同时将违背开放封闭原则。将来,若是网速很是快,再也不须要loading了,那么咱们要移除loading,就必须修改myImage对象。
实际上,myImage对象中,只须要实现给img标签添加src的功能。loading功能和过滤功能只是锦上添花。若是能把这些加强功能放在另外一个对象里面,天然是极好的设计。因而代理的做用在这里就体现出来了。代理加强过滤标签和loading功能,操做完成后,把请求从新交给本体myImage。
代理对象和本体对象的接口(参数)应该保持一致。 上述例子中,若是不须要加强功能的时候,咱们彻底可使用myImage对象替换proxyImage对象。在客户看来,代理对象和本体是一致的,客户并不须要知道代理和本体的区别,这样有两个好处。
第二点让我想到了里氏替换原则。
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类能够出现的地方,子类必定能够出现。
代理类能够看作是继承了目标类,并对其进行了加强。
此外,上面一直在谈论代理对象。注意:函数也是一个对象。
const myImage = (ele, src) => ele.src = src;
const proxyImage = (ele, src) => {
// loading功能,省略
myImage(ele, src);
}
复制代码
若是页面上有n多个checkbox,点击一个checkbox都要发送一个请求,请求携带checkbox的uniqueId参数。频繁的网络请求会带给服务器压力。最初的代码是这样的:
const postRequest = id => {
// 发送请求操做,忽略
}
const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
checkbox[i].onClick = function() {
postMessage(this.unique_id);
}
}
复制代码
那么怎样经过虚拟代理合并呢。
const postRequest = id => {
// 发送请求操做,忽略
}
const proxyPostRequest = (() => {
const caches = [];
let timer;
return id => {
caches.push(id);
if(timer) {
return;
}
timer = setTimeout(() => {
postRequest(caches.join(','));
caches.length = 0;
timer = null;
}, 2000);
}
})()
const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
checkbox[i].onClick = function() {
proxyPostRequest(this.unique_id);
}
}
复制代码
proxyPostRequest是一个IIFE,返回一个闭包。请求不要同时发出,而是两秒后合并id,只发送一次。
proxyPostRequest应用了函数柯里化(function currying)的思想。
currying又称为部分求值。一个currying的函数首先会接受一些参数,接受了这些参数以后,并不会当即求值,而是继续返回另一个函数,刚才传入的参数在函数造成的闭包中被保存起来。待到函数被真正须要求值的时候,以前传入的全部参数都会被一次性用于求值。
缓存代理能够为一些开销大的运算结果提供暂时的存储。在下次计算时,若是传递进来的参数跟以前一致,则能够直接返回以前缓存的结果。这须要不含反作用的函数(若是函数中有Date.now()、Math.random()、外部变量等参与了计算,那么可能会致使缓存的结果并不正确)。
// 假设这里的add有巨大的计算量(狗头)
var add = (...args) => {
return args.reduce((prev, curr) => {
return prev += curr;
}, 0)
}
const proxyAdd = (() => {
const caches = [];
return (...args) => {
let value = caches[args.join(',')];
if(value !== undefined) {
return value;
}
return caches[args.join(',')] = add(...args);
}
})()
proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3, 4);
复制代码
实际开发中,如某些展现性的表格,分页的数据不须要重复拉取。拉取一次后,换缓存下来,下次使用能够直接访问了。react开发中能够避免重复调动action。
// action/xxx.js
const fetchPageData = (id) => (() => {
const caches = [];
return dispatch => {
if(caches[id] !== undefined) {
return;
}
var data = fetchxxx(id);
if(data) {
caches[id] = id;
dispatch(storeData({
type: xxx,
data,
}))
}
return data;
}
})()
复制代码
显然这里可使用缓存代理达到请求。
上述缓存加速结果例子中,只能缓存加法的结果。若是须要缓存乘法的结果,那么又要建立一个proxyMulti的函数。这会写重复代码。可使用工厂模式来建立缓存代理。
return args.reduce((prev, curr) => {
return prev += curr;
}, 0)
}
const multi = (...args) => {
return args.reduce((prev, curr) => {
return prev *= curr;
}, 1)
}
const createProxyFactory = fn => {
const caches = [];
return (...args) => {
let value = caches[args.join(',')];
if(value !== undefined) {
return value;
}
return caches[args.join(',')] = fn.apply(this, args);
}
}
const proxyAdd = createProxyFactory(add);
proxyAdd(1, 2, 3, 4);
const proxyMulti = createProxyFactory(multi);
proxyMulti(1, 2, 3, 4);
复制代码
代理模式的变种很是多,限于篇幅以及在js的适用性,一下代理简单介绍一下。
代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。
优势:
缺点:
和其余模式的区别
一、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
二、和装饰器模式的区别:装饰器模式为了加强功能,而代理模式是为了加以控制(文中给图片鞥家loading的时候,彷佛区分不是那么明显)。
代理模式分类庞杂,在JS中最经常使用的是保护代理、虚拟代理和缓存代理(文中都用到了)。虽然代理模式很是有用,但不须要预先猜想是否须要使用代理,当发现不方便直接访问某个对象的时候,再编写代理也不迟。