代理模式是一种结构化设计模式(小灰的文章认为也可以算作是行为型设计模式),代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用,这就是代理模式的定义。通俗的讲,代理模式的核心是在被调用方和调用方之间增加一个中介者的角色,也就是代理。
现实生活中,比如我们有租房需求,可能就需要经过房屋中介,让我们认识能够找到合适的房东。
在求职高薪岗位的时候,我们也需要找到猎头,给我们推荐合适的公司。
代理模式在现实生活中无处不在……
当然,你觉得代理模式可能会让简单的事情变复杂,但中介者的角色实际上会给你减少很多麻烦和成本,在代码中代理模式可以避免对业务类的侵入,把日志、事务之类和业务无关的辅助功能单独拎出来。
代理模式有以下两个优点:
中介隔离作用:
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
符合代码设计的开放-封闭原则:
代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开放-封闭原则。
代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。
真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
代理模式虽然和装饰者模式很相似,但是装饰器模式会对装饰对象增加功能,而代理模式并不会对源对象有改变,从外层去操作了这个对象,对象本身是不会有其他的改变。
代理模式的缺点主要是增加了系统的复杂度,要斟酌当前场景是不是真的需要引入代理模式。
实现代理模式
这里用《JavaScript设计模式与开发实践》一书中的例子。
小明想要追一个小姐姐,想给小姐姐送一束花表白。在不使用代理模式和使用代理模式的简单例子。
不使用代理模式
ES5
// 鲜花类
var Flower = function(){};
// 小明
var xiaoming = {
sendFlower: function( target ){
var flower = new Flower();
target.receiveFlower( flower );
}
};
// 小姐姐
var cuteGirl = {
receiveFlower: function( flower ){
console.log( '收到花 ' + flower );
}
};
// 小明将鲜花直接交给小姐姐
xiaoming.sendFlower( cuteGirl );
ES6
// 鲜花类
class Flower {
}
// 小明
let xiaoming = {
sendFlower(target){
let flower = new Flower();
target.receiveFlower( flower );
}
}
// 小姐姐
let cuteGirl = {
receiveFlower(flower){
console.log( '收到花 ' + flower );
}
}
// 小明将鲜花直接交给小姐姐
xiaoming.sendFlower( cuteGirl );
使用代理模式
以上就是不使用代理模式的例子,小明在不了解小姐姐喜好的情况下,贸然直接表白被拒绝的概率非常大,但是小明的好朋友恰好认识小姐姐的舍友,舍友会在小姐姐心情好的时候,帮小明把鲜花转交给小姐姐。
ES5
// 鲜花类
var Flower = function(){};
// 小明
var xiaoming = {
sendFlower: function( target){
var flower = new Flower();
target.receiveFlower( flower );
}
};
// 好朋友
var goodFriend = {
receiveFlower: function( flower ){
cuteGirl.listenGoodMood(function(){ // 监听A的好心情
cuteGirl.receiveFlower( flower );
});
}
};
// 小姐姐
var cuteGirl = {
receiveFlower: function( flower ){
console.log( '收到花 ' + flower );
},
listenGoodMood: function( fn ){
setTimeout(function(){ // 假设10秒之后A的心情变好
fn();
}, 10000 );
}
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( goodFriend );
ES6
// 鲜花类
class Flower {
}
// 小明
let xiaoming = {
sendFlower(target){
let flower = new Flower();
target.receiveFlower( flower );
}
};
// 好朋友
let goodFriend = {
receiveFlower(flower){
cuteGirl.listenGoodMood(() => { // 监听A的好心情
cuteGirl.receiveFlower( flower );
});
}
};
// 小姐姐
let cuteGirl = {
receiveFlower( flower ){
console.log( '收到花 ' + flower );
},
listenGoodMood( fn ){
setTimeout(() => { // 假设10秒之后A的心情变好
fn();
}, 10000 );
}
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( goodFriend );
使用代理模式中的保护代理
保护代理用于对象应该具有不同访问权限的场景,控制对原始对象的访问。
还是用上述书中的例子,因为小明和小姐姐的舍友是好朋友,她了解小明的为人,所以愿意为小明转送鲜花给小姐姐。
而如果把小明换成一个不相干的人,那么小姐姐的舍友不可能会答应这个奇怪的请求。
但是在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。所以我们对下面的例子进行简单的改造,给鲜花类增加来源,从而实现简单的保护代理。
// 鲜花类
class Flower {
constructor(source) {
this.source = source;
}
}
// 小明
let xiaoming = {
sendFlower(target){
let flower = new Flower('xiaoming');
target.receiveFlower( flower );
}
};
// 路人
let passerby = {
sendFlower(target){
let flower = new Flower('passerby');
target.receiveFlower( flower );
}
};
// 小姐姐的闺蜜
let ladybro = {
receiveFlower(flower){
if (flower.source === 'xiaoming') {
cuteGirl.listenGoodMood(() => { // 监听A的好心情
cuteGirl.receiveFlower( flower );
});
} else {
throw new Error('小姐姐的闺蜜拒绝帮你送花!')
}
}
};
// 小姐姐
let cuteGirl = {
receiveFlower( flower ){
console.log( '收到花 ' + flower );
},
listenGoodMood( fn ){
setTimeout(() => { // 假设10秒之后A的心情变好
fn();
}, 10000 );
}
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( ladybro );
// 路人将鲜花交给小姐姐的闺蜜,委托她在小姐姐心情好的时候将鲜花转交给小姐姐
passerby.sendFlower( ladybro );
使用代理模式中的虚拟代理
还是用上面书中的例子,鲜花的种类有很多种,每种鲜花的售价也不近相同,不同的鲜花也有不同的保质期。
小明为了夺得小姐姐的欢心,希望小姐姐的闺蜜在小姐姐心情好的时候,再去帮忙购买一束比较昂贵的鲜花转送给小姐姐,此时的操作就叫虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
// 小姐姐的闺蜜
let ladybro = {
receiveFlower(flower){
if (flower.source === 'xiaoming') {
cuteGirl.listenGoodMood(() => { // 监听A的好心情
let flower = new Flower('xiaoming'); // 延迟创建flower 对象
cuteGirl.receiveFlower( flower );
});
} else {
throw new Error('小姐姐的闺蜜拒绝帮你送花!')
}
}
};
常见的虚拟代理实现
图片预加载
这里也是引用书中的例子,常见的开发需求之一,在图片未加载回来之前,希望有一个loading图进行占位,等loading图加载回来后再填充到img节点。
未使用代理模式
let MyImage = (function(){
let imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
// 创建一个Image对象,用于加载需要设置的图片
let img = new Image;
img.onload = function(){
// 监听到图片加载完成后,设置src为加载完成后的图片
imgNode.src = img.src;
};
return {
setSrc: function( src ){
// 设置图片的时候,设置为默认的loading图
imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif';
// 把真正需要设置的图片传给Image对象的src属性
img.src = src;
}
}
})();
MyImage.setSrc( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );
以上是未使用代理模式的写法,这也是常常容易写出来的代码情况,它在实现业务上并没有什么问题,但是MyImage
对象除了负责给img
节点设置src
外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
它同时还违反了开放—封闭原则,根据开放—封闭原则:
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
英文预加载loading的这个功能,是耦合进MyImage
对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在MyImage
对象里面改动代码。虽然MyImage
改动代码只需要几行就可以解决问题,但是换做其他甚至拥有10万行代码级别的JavaScript项目,要修改它的源代码风险就很大了。
使用代理模式
// 图片本地对象,负责往页面中创建一个img标签,并且提供一个对外的setSrc接口
let myImage = (function(){
let imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
//setSrc接口,外界调用这个接口,便可以给该img标签设置src属性
setSrc: function( src ){
imgNode.src = src;
}
}
})();
// 代理对象,负责图片预加载功能
let proxyImage = (function(){
// 创建一个Image对象,用于加载需要设置的图片
let img = new Image;
img.onload = function(){
// 监听到图片加载完成后,给被代理的图片本地对象设置src为加载完成后的图片
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
// 设置图片时,在图片未被真正加载好时,以这张图作为loading,提示用户图片正在加载
myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );
在使用了代理模式后:
图片本地对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口;
代理对象负责在图片未加载完成之前,引入预加载的loading图,负责了图片预加载的功能;
同时,它也满足了开放—封闭原则的基本思想:
开放—封闭原则的基本思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。
我们并没有改变或者增加MyImage
的接口,但是通过代理对象,实际上给系统添加了新的行为(这里的行为是图片预加载)。
合并HTTP请求
这里也是引用一个书中的例子,例如我们需要做一个文件同步的功能,在选中对应的文件时,需要被同步到自己的OneDrive。
这里把OneDrive中的同步文件夹替换成网页中的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>
未使用代理模式
// 同步文件请求的网络操作函数
let synchronousFile = function( id ){
console.log( '开始同步文件,id为: ' + id );
};
// 页面中所有的checkbox的选择器(因为上述的input的type只有checkbox,所以此时可以全部选中)
let checkbox = document.getElementsByTagName( 'input' );
// 遍历checkbox选择器
for ( let i = 0, c; c = checkbox[ i++ ]; ){
// 循环遍历添加点击事件,点击后如果是选中状态,则触发同步文件请求
c.onclick = function(){
if ( this.checked === true ){
synchronousFile( this.id );
}
}
};
在未使用代理模式时,每选中一次checkbox,就会触发一次同步文件请求,频繁的网络请求,会给服务器带来比较大的开销,此时我们可以在不改变synchronousFile
函数职能的情况下,将它进行代理。
使用代理模式
// 同步文件请求的网络操作函数
let synchronousFile = function( id ){
console.log( '开始同步文件,id为: ' + id );
};
// 同步文件请求的网络操作函数-代理函数
let proxySynchronousFile = (function(){
let 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 );
}
})();
// 页面中所有的checkbox的选择器(因为上述的input的type只有checkbox,所以此时可以全部选中)
let checkbox = document.getElementsByTagName( 'input' );
// 遍历checkbox选择器
for ( let i = 0, c; c = checkbox[ i++ ]; ){
c.onclick = function(){
// 循环遍历添加点击事件,点击后如果是选中状态,则触发同步文件请求的代理函数
if ( this.checked === true ){
proxySynchronousFile( this.id );
}
}
};
synchronousFile
函数被代理后的函数我们起名为proxySynchronousFile
,它增加了一个缓存数组,所有两秒内的checkbox选中,都会被添加到缓存数组check中,等待2秒之后才把这2秒之内需要同步的文件ID一次性全打包发给服务器(将多个id拼接成逗号分割的字符串),在实时性要求不是很高的系统,这能大大减少服务器的压力。
惰性加载中的应用
来自于书中的例子,假设有一个迷你控制台的项目——miniConsole.js,它有一个log函数,专门用于打印参数。
// miniConsole.js代码
let miniConsole = {
log: function(){
// 真正代码略
console.log( Array.prototype.join.call( arguments ) );
}
};
export default miniConsole
因为这个控制台项目,是只在控制台展示的时候才需要的,我们希望他在有必要的时候才开始加载它,比如按F2的时候,加载miniConsole.js,就可以使用代理模式,惰性加载miniConsole.js。
大致的步骤是:
在用户敲击F2的时候,才去动态引入miniConsole.js的script标签
在用户敲击F2之前执行过的log命令,都会被缓存到代理对象内部的cache缓存数组内
等动态引入miniConsole.js的操作完成后,再从中逐一取出并执行。
详细代码如下:
// proxyMiniConsole.js代码
// miniConsole的代理对象
let proxyMiniConsole = (function(){
// 存储每次执行log时的回调函数
let cache = [];
let handler = function( ev ){
// 如果用户按了F2唤出了控制台
if ( ev.keyCode === 113 ){
// 执行引入miniConsole.js的操作
let script = document.createElement( 'script' );
script.src = 'miniConsole.js';
document.getElementsByTagName( 'head' )[0].appendChild( script );
document.body.removeEventListener( 'keydown', handler );// 只加载一次miniConsole.js
script.onload = function(){
// 如果miniConsole.js的script标签引入并加载完成
for ( var i = 0, fn; fn = cache[ i++ ]; ){
// 遍历所有缓存的回调函数并执行
fn();
}
};
}
};
// 监听键盘按键敲击事件
document.body.addEventListener( 'keydown', handler, false );
return {
// 返回代理后的方法
log: function(){
// 获取传入的所有参数
let args = arguments;
// 向缓存列表加入要打印的参数
cache.push( function(){
return miniConsole.log.apply( miniConsole, args );
});
}
}
})();
miniConsole.log( 11 ); // 开始打印log
使用代理模式中的缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
计算乘积
/**************** 计算乘积 *****************/
let mult = function(){
let a = 1;
for ( let i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
/**************** 计算加和 *****************/
let plus = function(){
let a = 0;
for ( let i = 0, l = arguments.length; i < l; i++ ){
a = a + arguments[i];
}
return a;
};
/**************** 创建缓存代理的工厂 *****************/
let createProxyFactory = function( fn ){
// 缓存计算后的结果
let cache = {};
return function(){
// 通过字符串拼接所有传入的参数
let args = Array.prototype.join.call( arguments, ',' );
// 如果这个参数存在缓存内
if ( args in cache ){
// 则直接返回缓存的结果
return cache[args];
}
// 否则再对这个值进行计算
return cache[args] = fn.apply( this, arguments );
}
};
let proxyMult = createProxyFactory( mult ),
proxyPlus = createProxyFactory( 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
参考资料
[CUG-GZ]前端知识进阶——代理模式
www.yuque.com/cuggz/feplu…
前端设计模式之代理模式
juejin.cn/post/684490…
漫画:什么是 “代理模式” ?
mp.weixin.qq.com/s/O8_A2Ms9M…
JavaScript设计模式与开发实践
www.ituring.com.cn/book/1632
从ES6重新认识JavaScript设计模式(五): 代理模式和Proxy
segmentfault.com/a/119000001…
使用 JavaScript 原生的 Proxy 优化应用
juejin.cn/post/684490…