代理模式: 为一个对象提供一个代用品或占位符, 以便控制它的访问.
当我们不方便直接访问某个对象时, 或不满足需求时, 可考虑使用一个替身对象来控制该对象的访问. 替身对象可对请求预先进行处理, 再决定是否转交给本体对象.
生活小栗子:
代购;
明星经纪人;
和谐上网
经常 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() , 进行表单校验并返回结果, 并且借用 "策略模式" 独立封装验证逻辑. 使得表单对象, 验证逻辑, 验证器各自独立. 代码整洁性, 维护性及复用性都得到增强.
来源: http://www.jianshu.com/p/3b102d3cf656