代理模式:为一个对象提供一个代用品或占位符,以便控制它的访问。
当我们不方便直接访问某个对象时,或不满足需求时,可考虑使用一个替身对象来控制该对象的访问。替身对象可对请求预先进行处理,再决定是否转交给本体对象。
生活小栗子:
- 代购;
- 明星经纪人;
- 和谐上网
经常 shopping 的同学,对代购应该不陌生。自己不方便直接购买或买不到某件商品时,会选择委托给第三方,让代购或黄牛去做购买动作。程序世界的代理者也是如此,我们不直接操作原有对象,而是委托代理者去进行。代理者的作用,就是对我们的请求预先进行处理或转接给实际对象。
模式特点
- 代理对象可预先处理请求,再决定是否转交给本体;
- 代理和本体对外显示接口保持一致性
- 代理对象仅对本体做一次包装
模式细分
- 虚拟代理(将开销大的运算延迟到需要时执行)
- 缓存代理(为开销大的运算结果提供缓存)
- 保护代理(黑白双簧,代理充当黑脸,拦截非分要求)
- 防火墙代理(控制网络资源的访问)
- 远程代理(为一个对象在不同的地址控件提供局部代表)
- 智能引用代理(访问对象执行一些附加操作)
- 写时复制代理(延迟对象复制过程,对象需要真正修改时才进行)
JavaScript 中常用的代理模式为“虚拟代理”和“缓存代理”。
模式实现
实现方式:创建一个代理对象,代理对象可预先对请求进行处理,再决定是否转交给本体,代理和本体对外接口保持一致性(接口名相同)。
// 例子:代理接听电话,实现拦截黑名单
var backPhoneList = ['189XXXXX140']; // 黑名单列表
// 代理
var ProxyAcceptPhone = function(phone) {
// 预处理
console.log('电话正在接入...');
if (backPhoneList.includes(phone)) {
// 屏蔽
console.log('屏蔽黑名单电话');
} else {
// 转接
AcceptPhone.call(this, phone);
}
}
// 本体
var AcceptPhone = function(phone) {console.log('接听电话:', phone);
};
// 外部调用代理
ProxyAcceptPhone('189XXXXX140');
ProxyAcceptPhone('189XXXXX141');
代理并不会改变本体对象,遵循 “单一职责原则”,即“自扫门前雪,各找各家”。不同对象承担独立职责,不过于紧密耦合,具体执行功能还是本体对象,只是引入代理可以选择性地预先处理请求。例如上述代码中,我们向“接听电话功能”本体添加了一个屏蔽黑名单的功能(保护代理),预先处理电话接入请求。
虚拟代理(延迟执行)
虚拟代理的目的,是将开销大的运算延迟到需要时再执行。
虚拟代理在图片预加载的应用,代码例子来至《JavaScript 设计模式与开发实践》
// 本体
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;
img.onload = function() {myImage.setSrc(this.src); // 图片加载完设置真实图片 src
}
return {setSrc: function(src) {myImage.setSrc('./loading.gif'); // 预先设置图片 src 为 loading 图
img.src = src;
}
}
})();
// 外部调用
proxyImage.setSrc('./product.png'); // 有 loading 图的图片预加载效果
缓存代理(暂时存储)
缓存代理的目的,是为一些开销大的运算结果提供暂时存储,以便下次调用时,参数与结果不变情况下,从缓存返回结果,而不是重新进行本体运算,减少本体调用次数。
应用缓存代理的本体,要求运算函数应是一个纯函数,简单理解比如一个求和函数 sum
,输入参数 (1, 1)
, 得到的结果应该永远是 2
。
纯函数:固定的输入,有固定的输出,不影响外部数据。
模拟场景:60 道判断题测试,每三道题计分一次,根据计分筛选下一步的三道题目?
三道判断题得分结果:
- (0, 0 ,0)
- (0, 0, 1)
- (0, 1, 0)
- (0, 1, 1)
- (1, 0, 0)
- (1, 0, 1)
- (1, 1, 0)
- (1, 1, 1)
总共七种计分结果。60/3 = 20
,共进行 20 次计分,每次计分执行 3 个循环累计,共 60 个循环。接下来,借用“缓存代理”方式,来实现最少本体运算次数。
// 本体:对三道题答案进行计分
var countScore = function(ansList) {let [a, b, c] = ansList;
return a + b + c;
}
// 代理:对计分请求预先处理
var proxyCountScore = (function() {var existScore = {}; // 设定存储对象
return function(ansList) {var attr = ansList.join(','); // eg. ['0,0,0']
if (existScore[attr] != null) {
// 从内存返回
return existScore[attr];
} else {
// 内存不存在,转交本体计算并存入内存
return existScore[attr] = countScore(ansList);
}
}
})();
// 调用计分
proxyCountScore([0,1,0]);
60 道题目,每 3 道题一次计分,共 20 次计分运算,但总的计分结果只有 7 种,那么实际上本体 countScore()
最多只需运算 7 次,即可囊括所有计算结果。
通过缓存代理的方式,对计分结果进行临时存储。用答案字符串组成属性名 ['0,1,0']
作为 key
值检索内存,若存在直接从内存返回,减少包含复杂运算的本体被调用的次数。之后如果我们的题目增加至 90 道,120 道,150 道题时,本体 countScore()
运算次数仍旧保持 7 次,中间节省了复杂运算的开销。
ES6 的 Proxy
ES6 新增的 Proxy
代理对象的操作,具体的实现方式是在 handler
上定义对象自定义方法集合,以便预先管控对象的操作。
ES6 的 Proxy 语法:let proxyObj = new Proxy(target, handler);
- target: 本体,要代理的对象
- handler: 自定义操作方法集合
- proxyObj: 返回的代理对象,拥有本体的方法,不过会被
handler
预处理
// ES6 的 Proxy
let Person = {name: '以乐之名'};
const ProxyPerson = new Proxy(Person, {get(target, key, value) {if (key != 'age') {return target[key];
} else {return '保密'}
},
set(target, key, value) {if (key === 'rate') {target[key] = value === 'A' ? '推荐' : '待提高'
}
}
})
console.log(ProxyPerson.name); // '以乐之名'
console.log(ProxyPerson.age); // '保密'
ProxyPerson.rate = 'A';
console.log(ProxyPerson.rate); // '推荐'
ProxyPerson.rate = 'B';
console.log(ProxyPerson.rate); // '待提高'
handler
除常用的 set/get
,总共支持 13 种方法:
handler.getPrototypeOf()
// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时
handler.setPrototypeOf()
// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时
handler.isExtensible()
// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时
handler.preventExtensions()
// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时
handler.getOwnPropertyDescriptor()
// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时
handler.defineProperty()
// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时
handler.has()
// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时
handler.get()
// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时
handler.set()
// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时
handler.deleteProperty()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时
handler.ownKeys()
// 在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时
handler.apply()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。handler.construct()
// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行 new proxy() 时
适用场景
-
虚拟代理:
- 图片预加载(loading 图)
- 合并 HTTP 请求(数据上报汇总)
-
缓存代理:(前提本体是纯函数)
- 缓存异步请求数据
- 缓存较复杂的运算结果
-
ES6 的 Proxy:
- 实现对象私有属性
- 实现表单验证
“策略模式”可应用于表单验证信息,“代理方式”也可实现。这里引用 Github – jawil 的一个例子,思路供大家分享。
// 利用 proxy 拦截格式不符数据
function validator(target, validator, errorMsg) {
return new Proxy(target, {
_validator: validator,
set(target, key, value, proxy) {
let errMsg = errorMsg;
if (value == null || !value.length) {console.log(`${errMsg[key]} 不能为空 `);
return target[key] = false;
}
let va = this._validator[key]; // 这里有策略模式的应用
if (!!va(value)) {return Reflect.set(target, key, value, proxy);
} else {console.log(`${errMsg[key]} 格式不正确 `);
return target[key] = false;
}
}
})
}
// 负责校验的逻辑代码
const validators = {name(value) {return value.length >= 6;},
passwd(value) {return value.length >= 6;},
moblie(value) {return /^1(3|5|7|8|9)[0-9]{9}$/.test(value);
},
email(value) {return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)
}
}
// 调用代码
const errorMsg = {
name: '用户名',
passwd: '密码',
moblie: '手机号码',
email: '邮箱地址'
}
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function () {let validatorNext = function* () {
yield vali.name = registerForm.userName.value
yield vali.passwd = registerForm.passWord.value
yield vali.moblie = registerForm.phone.value
yield vali.email = registerForm.email.value
}
let validator = validatorNext();
for (let field of validator) {validator.next();
}
}
实现思路:利用 ES6 的 proxy 自定义 handler
的 set()
,进行表单校验并返回结果,并且借用“策略模式 ” 独立封装验证逻辑。使得表单对象,验证逻辑,验证器各自独立。代码整洁性,维护性及复用性都得到增强。
关于“设计模式”在表单验证的应用,可参考 jawil 原文:《探索两种优雅的表单验证——策略设计模式和 ES6 的 Proxy 代理模式》。
优缺点
-
优点:
- 可拦截和监听外部对本体对象的访问;
- 复杂运算前可以进行校验或资源管理;
- 对象职能粒度细分,函数功能复杂度降低,符合“单一职责原则”;
- 依托代理,可额外添加扩展功能,而不修改本体对象,符合“开发 - 封闭原则”
-
缺点:
- 额外代理对象的创建,增加部分内存开销;
- 处理请求速度可能有差别,非直接访问存在开销,但“虚拟代理”及“缓存代理”均能提升性能
参考文章
- 《JavaScript 设计模式与开发实践》
- 《ES6 中的代理模式 —–Proxy》
- 《探索两种优雅的表单验证——策略设计模式和 ES6 的 Proxy 代理模式》
Github,期待 Star!
https://github.com/ZengLingYong/blog
作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。