代理模式
# 代理模式
代理模式(Proxy Pattern) 是指为一个原对象找一个代理对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。代理模式的英文叫做 Proxy 或 Surrogate,它是一种对象结构型模式。
最常见的例子就是经纪人代理明星业务,假设你作为投资人,想联系明星打广告,那么你就需要先经过代理经纪人,经纪人对你的资质进行考察,并为你进行排期,替明星过滤不必要的信息。
事件委托/代理、jQuery 的 $.proxy
、ES6 的 proxy
都是这一模式的实现。
# 代理模式分类
代理模式又分为 静态代理 和 动态代理:
静态代理 是由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的
.class
文件就已经存在了。动态代理 是在程序运行时,通过运用反射机制动态的创建而成。
# 模式结构
代理模式包含如下角色:
- Subject(抽象主题角色):声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象。
- Proxy(代理主题角色):也称为委托角色或者被代理角色。定义了代理对象所代表的目标对象。
- RealSubject(真实主题角色):也叫委托类、代理类。代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后,执行某个操作,而不是单纯地将调用传递给目标对象。
# 优点和缺点
代理模式的优点
代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
保护代理可以控制对真实对象的使用权限。
代理模式的缺点
由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
# 代理模式的应用场景
# 1.事件代理
事件本身具有“冒泡”的特性,当我们点击子元素时,点击事件会“冒泡”到父元素 div 上,从而被监听到。如此一来,点击事件的监听函数只需要在 父元素 div 元素上被绑定一次即可,而不需要在子元素上被绑定 N 次——这种做法就是事件代理,它可以很大程度上提高我们代码的性能。
// 获取父元素
const father = document.getElementById("father");
// 给父元素安装一次监听函数
father.addEventListener("click", function(e) {
// 识别是否是目标子元素
if (e.target.tagName === "A") {
// 以下是监听函数的函数体
e.preventDefault();
alert(`我是${e.target.innerText}`);
}
});
2
3
4
5
6
7
8
9
10
11
12
在这种做法下,我们的点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。
# 2.缓存代理
缓存代理比较好理解,它应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。
应用场景 1:阶乘
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前端存储的结果。
// 1. 阶乘(专注于自身职责,阶乘,缓存由代理实现)
function factorial(n) {
console.log("重新计算阶乘");
if (n < 2) {
return n;
} else {
return n * factorial(n - 1);
}
}
function proxy(fn) {
const catchObject = {};
return function(key) {
if (key in catchObject) {
return catchObject[key];
} else {
return (catchObject[key] = fn(key));
}
};
}
const instance = proxy(factorial);
instance(3);
instance(3);
instance(3);
//这时候我们发现无论 “重复” 执行多少次, 都只计算了一次
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
应用场景 2:tab 切换
<div class="tab" onClick="handleClick">
<span> 1 </span>
<span> 2 </span>
<span> 3 </span>
</div>
<div class="content">
<!-- 根据tab获取不同数据显示在这 -->
</div>
class ProxyTab {
static catch = {}
constructor(target) {
if (!ProxyTab.catch[target]) {
ProxyTab.catch[target] = this.http()
}
}
http() {
return axios.get('xxx')
}
}
function handleClick(e) {
new ProxyTab(e.target)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 3.虚拟代理
将一些代价昂贵的操作放置在代理对象中,待到机会合适时再进行,这种代理就叫虚拟代理
应用场景 :图片懒加载
在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,
由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张
loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。
虚拟代理:作为创建开销大的对象的代表;虚拟代理经常直到我们真正需要一个对象的时候才创建它;当对象在创建或创建中时,由虚拟代理来扮演对象的替身;对象创建后,代理就会将请求直接委托给对象。
const image = (function() {
const imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
},
};
})();
// 代理容器
const proxyImage = (function() {
let img = new Image();
// 加载完之后将设置为添加的图片
img.onload = function() {
image.setSrc(this.src);
};
return {
setSrc: function(src) {
image.setSrc("loading.gif");
img.src = src;
},
};
})();
proxyImage.setSrc("file.jpg");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
代理容器控制了客户对 Image 的访问,并且在过程中加了一些额外的操作。
# 4.保护代理
在代理模式中,替身对象能做到过滤一些对本体不合理的请求时,这种代理就叫保护代理
// 保护代理
function Flower() {}
function Person(name, age, salary) {
this.age = age;
this.name = name;
this.salary = salary;
}
Person.prototype.sendFlower = function(target, person) {
var flower = new Flower();
target.receive(flower, person);
};
var person1 = new Person("www", 20, 4000);
var person2 = new Person("AAA", 25, 8000);
var person3 = new Person("BBB", 45, 16000);
var proxyObj = {
receive: function(flower, person) {
if (person.age >= 40) {
console.log(person.name + ",你年龄太大了");
return false;
}
if (person.salary < 5000) {
console.log(person.name + ",你工资太低了");
return false;
}
originObj.receive(flower);
console.log(person.name + ",恭喜你,女神收下了你的花");
},
};
var originObj = {
receive: function(flower) {},
};
person1.sendFlower(proxyObj, person1); // 输出www,你工资太低了
person2.sendFlower(proxyObj, person2); // 输出AAA,恭喜你,女神收下了你的花
person3.sendFlower(proxyObj, person3); // 输出BBB,你年龄太大了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
参考:前端 JS 高频面试题---3.代理模式 (opens new window)
学习 JavaScript 设计模式 - 代理模式 (opens new window)
# 5.其它代理
代理模式的变体种类非常多,限于篇幅及其在 JavaScript 中的适用性,本章只简约介绍一下这些代理,就不一一详细展开说明了。
防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
远程代理:为一个对象在不同的地址空间提供局部代表,在 Java 中,远程代理可以是另一个虚拟机中的对象。
智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。