代理模式
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
在面向对象的编程中,代理模式的合理使用能够很好的体现下面两条原则:
- 单一职责原则: 面向对象设计中鼓励将不同的职责分布到细粒度的对象中,Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。
- 开放-封闭原则:代理可以随时从程序中去掉,而不用对其他部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种原因不再需要代理,那么就可以容易的将代理对象换成原对象的调用
ES6中的代理模式
ES6所提供Proxy构造函数能够让我们轻松的使用代理模式。
var proxy = new Proxy(target, handler);
Proxy构造函数传入两个参数,第一个参数target表示所要代理的对象,第二个参数handler也是一个对象用来设置对所代理的对象的行为。
利用Proxy实现前端中3种代理模式的使用场景,分别是:缓存代理、验证代理、实现私有属性,事件代理,代理图片预加载。
缓存代理
缓存代理可以将一些开销很大的方法的运算结果进行缓存,再次调用该函数时,若参数一致,则可以直接返回缓存中的结果,而不用再重新进行运算。
const getFib = (number) => {
if (number <= 2) {
return 1;
} else {
return getFib(number - 1) + getFib(number - 2);
}
}
使用缓存代理的工厂函数:
const getFib = (number) => {
if (number <= 2) {
return 1;
} else {
return getFib(number - 1) + getFib(number - 2);
}
}
function getCacheProxy(fn, map){
return new Proxy(fn, {
apply: (target, context, args)=> {
const argString = args.join(' ');
if(map.has(argString)) {
return map.get(argString);
}
let res = Reflect.apply(target, context, args);
map.set(argString, res);
return res;
}
});
}
// 使用
let map = new Map();
let proxyFib = getCacheProxy(getFib, map);
let res1 = proxyFib(10);
let re2 = proxyFib(10);
当我们第二次调用getFibProxy(40)时,getFib函数并没有被调用,而是直接从cache中返回了之前被缓存好的计算结果。通过加入缓存代理的方式,getFib只需要专注于自己计算斐波那契数列的职责,缓存的功能使由Proxy对象实现的。这实现了我们之前提到的单一职责原则。
验证代理
Proxy构造函数第二个参数中的set方法,可以很方便的验证向一个对象的传值。我们以一个传统的登陆表单举例,该表单对象有两个属性,分别是account和password,每个属性值都有一个简单和其属性名对应的验证方法,验证规则如下:
// 表单对象
const userForm = {
account: '',
password: '',
};
// 验证方法
const validators = {
account(value) {
// account 只允许为中文
const re = /^[\u4e00-\u9fa5]+$/;
return {
valid: re.test(value),
error: '"account" is only allowed to be Chinese'
}
},
password(value) {
// password 的长度应该大于6个字符
return {
valid: value.length >= 6,
error: '"password" should more than 6 character'
};
}
};
使用Proxy实现一个通用的表单验证器:
const getValidateProxy = (target, validators) => {
return new Proxy(target, {
set(target, key, value, receiver){
if(!value) {
console.error(`"${key}" is not allowed to be empty`);
return target[key] = false;
}
const validResult = validators[key](value);
if(validResult.valid) {
const res = Reflect.set(target, key, value, receiver);
return res;
}
else {
console.error(validResult.error);
return target[key] = false;
}
}
});
}
let vp = getValidateProxy(userForm, validators);
vp.password = '123';
vp.account = 'david';
实现私有属性
代理模式还有一个很重要的应用是实现访问限制。总所周知,JavaScript是没有私有属性这一个概念的,通常私有属性的实现是通过函数作用域中变量实现的,虽然实现了私有属性,但对于可读性来说并不好。
私有属性一般是以_下划线开头,但ES6中可以使用#来实现私有属性。
function getPrivateProps(obj, isPrivate){
return new Proxy(obj, {
get(target, key, receiver){
if(!isPrivate(key)) {
let res = Reflect.get(target, key, receiver);
if(typeof res === 'function') {
res = res.bind(obj);
}
return res;
}
},
set(target, key, value, receiver){
if(isPrivate(key)) {
throw new TypeError('Cannot set ' + key);
}
return Reflect.set(target, key, value, receiver);
},
has(target, key, receiver){
return isPrivate(prop) ? false : Reflect.has(target, key, receiver);
},
getOwnPropertyDescriptor(target, key, receiver){
return isPrivate(prop) ? undefined : Reflect.getOwnPropertyDescriptor(target, key, receiver);
}
});
}
function isPrivate(key){
return key.startsWith('_');
}
const myObj = {
public: 'hello',
_privat1: 'secret1',
_privat2: 'secret2',
output: function () {
console.log(this._private1);
},
_print: function () {
console.log(this._private2);
}
};
const proxy = getPrivateProps(myObj, isPrivate);
console.log(proxy.public);
console.log(proxy._privat1);
proxy._privat1 = 'Hello';
proxy._print();
事件代理
HTML元 素事件代理。
<ul id="ul">
<div>111</div>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#ul');
ul.addEventListener('click', event => {
console.log(event.target);
});
</script>
react 的事件机制利用了事件委托机制。事件并没有绑定在真实的 dom 节点上,而是把事件都绑定在结构的最外层 document,使用一个统一的事件监听器。所有的事件都由这个监听器统一分发。这样的事件机制简单而又高效。
虚拟代理实现图片预加载
导致图片出来前会有一片空白。所以我们限用一张 loading 图片占位,在异步方式加载图片。
不用代理:
// 创建一个本体对象
var myImage = (function(){
// 创建标签
var imgNode = document.createElement( 'img' );
// 添加到页面
document.body.appendChild( imgNode );
return {
// 设置图片的src
setSrc: function( src ){
// 更改src
imgNode.src = src;
}
}
})();
myImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
使用代理:
// 创建一个本体对象
var myImage = (function(){
// 创建标签
var imgNode = document.createElement( 'img' );
// 添加到页面
document.body.appendChild( imgNode );
return {
// 设置图片的src
setSrc: function( src ){
// 更改src
imgNode.src = src;
}
}
})();
// 创建代理对象
var proxyImage = (function(){
// 创建一个新的img标签
var img = new Image;
// img 加载完成事件
img.onload = function(){
// 调用 myImage 替换src方法
myImage.setSrc( this.src );
}
return {
// 代理设置地址
setSrc: function( src ){
// 预加载 loading
myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
// 赋值正常图片地址
img.src = src;
}
}
})();
proxyImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
优缺点
优点
- 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
- 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
处理请求速度可能有差别,非直接访问存在开销。
总结
ES6提供的Proxy可以让JS开发者很方便的使用代理模式,虽然代理模式很方便,但是在业务开发时应该注意使用场景,不需要在编写对象时就去预先猜测是否需要使用代理模式,只有当对象的功能变得复杂或者我们需要进行一定的访问限制时,再考虑使用代理。
我的微信公众号
更多精彩文章请关注我的前端技术公众号哦!