代理模式:为一个对象提供一个代用品或占位符,以便控制它的访问。
当咱们不方便直接访问某个对象时,或不知足需求时,可考虑使用一个替身对象来控制该对象的访问。替身对象可对请求预先进行处理,再决定是否转交给本体对象。javascript
生活小栗子:java
常常 shopping 的同窗,对代购应该不陌生。本身不方便直接购买或买不到某件商品时,会选择委托给第三方,让代购或黄牛去作购买动做。程序世界的代理者也是如此,咱们不直接操做原有对象,而是委托代理者去进行。代理者的做用,就是对咱们的请求预先进行处理或转接给实际对象。git
JavaScript 中经常使用的代理模式为 “虚拟代理” 和 “缓存代理”。github
实现方式:建立一个代理对象,代理对象可预先对请求进行处理,再决定是否转交给本体,代理和本体对外接口保持一致性(接口名相同)。
// 例子:代理接听电话,实现拦截黑名单 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图的图片预加载效果
缓存代理的目的,是为一些开销大的运算结果提供暂时存储,以便下次调用时,参数与结果不变状况下,从缓存返回结果,而不是从新进行本体运算,减小本体调用次数。app
应用缓存代理的本体,要求运算函数应是一个纯函数,简单理解好比一个求和函数 sum
, 输入参数 (1, 1)
, 获得的结果应该永远是 2
。异步
纯函数:固定的输入,有固定的输出,不影响外部数据。
模拟场景:60道判断题测试,每三道题计分一次,根据计分筛选下一步的三道题目?函数
三道判断题得分结果:
总共七种计分结果。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
代理对象的操做,具体的实现方式是在 handler
上定义对象自定义方法集合,以便预先管控对象的操做。
ES6 的 Proxy语法:let proxyObj = new Proxy(target, handler);
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() 时
虚拟代理:
缓存代理:(前提本体是纯函数)
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代理模式》。
优势:
缺点:
参考文章
Github,期待Star!
https://github.com/ZengLingYong/blog
做者:以乐之名 本文原创,有不当的地方欢迎指出。转载请指明出处。