代理模式在生活中非常的常见,比如你想卖房子有房产代理人,明星有经纪人可以代理他们的一些事物,外卖小哥也在商家和你之间作为一种代理人,把外卖送到你的手上...
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。 如图:
四月是春花烂漫的季节,张楚也唱到孤独的人是可耻的,这不在一个明媚的早晨,李四在公园里遇见了让他小鹿乱撞的女孩,他立马就走不动路了,于是他鼓足勇气上去搭讪要微信,迎接他的确是一顿大耳刮子。
class WxNumber {...} class LeeSi { askWx(target) { const res = target.getRequest() console.log(res) } } class Beauty { getRequest() { return '一顿大耳刮子' } } const lee = new LeeSi() lee.askWx(new Beauty()) 复制代码
第一次的求爱不成功,但是李四并不气馁,因为他是一个痴情种子。他多方面打听到原来他的一个好朋友张代丽原来是女神的闺蜜。于是他请张代丽出马,帮他要微信号。
class WxNumber {...} const wxNum = new WxNumber() class LeeSi { askWx(target) { const res = target.getRequest() console.log(res) } } class ZhangProxy { getRequest() { const beauty = new Beauty() return beauty.getRequest() } } class Beauty { getRequest() { return wxNum } } const lee = new LeeSi() lee.askWx(new ZhangProxy()) 复制代码
就这样李四终于得到了女神的微信。
虽然是一个简单的例子,但是我们可以从中得到两种代理模式的身影,保护代理和虚拟代理。
class Beauty { getRequest() { return new WxNumber() } } 复制代码
再来看一个例子:
在一些网站中,有时候网速不好的时候,网页中的图片会在打开的时候出现一段时间的白屏,这时候我们一般会通过预加载的技术,先在图片的位置放置一张loading图,等到图片加载好了的时候,再将图片显示出来。
我们先来实现一个本体类
class MyImg { constructor() { this.imgNode = document.createElement('img') } addImgNode() { document.body.appendChild(this.imgNode) } setSrc(src) { this.imgNode.src = src this.addImgNode() } } 复制代码
然后我们引入一个代理对象ProxyImg,通过这个代理对象,在图片被真正加载好之前,页面将出现一张占位图来告诉用户正在加载。
class ProxyImg { constructor() { this.myImg = new MyImg() this.img = new Image this.src = null this.img.onload = () => { this.myImg.setSrc(this.src) } } setSrc(src) { this.MyImg.setSrc('xxx/xxx/aa.gif') this.img.src = src this.src = src } } 复制代码
现在我们通过ProxyImg间接地访问MyImg, ProxyImg控制了客户对MyImg的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把img节点的src设置为一张本地的loading图片。
也许有人就会说,不过是一个小小的图片预加载的功能,即使不使用任何的设计模式,我分分钟手撸一个出来。那么代理模式的作用到底体现在什么地方呢?
我觉得的是一个面向对象设计的原则:单一职责原则。
单一职责原则指的是,就一个类而言,应该仅有一个 引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个
比如我们不用代理实现一个图片预加载的类:
class MyImg { constructor() { this.imgNode = document.createElement('img') this.img = new Image this.img.onload = this.loadImg() this.src = null } loadImg() { this.imgNode.src = this.src } addImgNode() { document.body.appendChild(this.imgNode) } setSrc(src) { this.imgNode.src = 'xxx/xxx/aa.gif' this.img.src = src this.src = src this.addImgNode() } } 复制代码
这段代码除了给img节点设置src外,还要负责预加载图片。当我们处理其中一个职责的时候,这就有可能因为其强耦合性影响另一个职责的实现。
还有一种情况,一些别的什么原因,可能5年后网速已经快的上天了,我们可以不用再进行代理了,那我们就直接去掉代理这一层就可以,也不用再去改MyImg这个类,这就又符合开放-封闭的原则了。
从这几个例子中我们可以看到一个规律,那就是代理暴露的方法名,和本体暴露的方法名是一致的。代理接收请求的过程对于用户来说是透明的,用户并不知道这其中的区别,这样做也就可以做到在使用本体的地方都可以替换成使用代理。
缓存代理可以为一些开销比较大的运算结果提供暂时的缓存,下次运算的时候,如果传递进来的参数和之前一致,则可以直接返回前面存储的运算结果。
我们先来实现一个用于求乘积的类,然后加入缓存代理
class Mult { cal() { console.log('开始计算') const res = [].reduce.call(arguments, ((cur, next) => { return cur*next }), 1) return res } } class ProxyMult { static cache = {} constructor() { this.mult = new Mult() } cal() { let args = [].join.call(arguments, ',') if (args in ProxyMult.cache) { return ProxyMult.cache[args] } return ProxyMult.cache[args] = this.mult.cal.apply(this, arguments) } } const proxyMult = new ProxyMult() console.log(proxyMult.cal(1,2,3,4)) console.log(proxyMult.cal(1,2,3,5)) console.log(proxyMult.cal(1,2,3,4)) 复制代码
可以很清楚的看到Mult类中的cal方法只执行了两次,所以缓存生效。
我们写代码的过程要时刻问问自己,什么是一直在变的,什么是不变的。变化的我们尽量遵循单一职责的原则实现分别的逻辑,不变的部分我们争取封装起来,让他遵循开放-封闭原则。
开放封闭原则说的是对扩展开放,对修改封闭
所以我们可以通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理,所以计算方法就是可变的。 我们再来创建一个计算加和的类和创建缓存代理的工厂
class Mult { constructor() { this.name = 'mult' } cal() { console.log('开始计算Mult') return [].reduce.call(arguments, ((cur, next) => { return cur*next }), 1) } } class Plus { constructor() { this.name = 'plus' } cal() { console.log('开始计算Plus') return [].reduce.call(arguments, ((cur, next) => { return cur + next }), 0) } } class CreateProxyFactory { static cache = {} constructor(fn) { this.fn = new fn() console.log(this.fn) } cal() { const args = [].join.call(arguments, `,${this.fn.name}`) if (args in CreateProxyFactory.cache) { return CreateProxyFactory.cache[args] } return CreateProxyFactory.cache[args] = this.fn.cal.apply(this, arguments) } } const proxyMult = new CreateProxyFactory(Mult) const ProxyPlus = new CreateProxyFactory(Plus) console.log(proxyMult.cal(1,2,3,4)) console.log(proxyMult.cal(1,2,3,4)) console.log(ProxyPlus.cal(1,2,3,4)) console.log(ProxyPlus.cal(1,2,3,4)) 复制代码
代理模式有很多种类,但是在js中的适用性都不太高,有兴趣的可以单独去找资料拿来学习。
下面我们来说说ES6新加的这个Api:Proxy,光看名字我们就能想到为什么写代理模式的时候也要讲一下这个Proxy。
在MDN上对于Proxy的解释是:
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。 复制代码
首先它的语法是:
let p = new Proxy(target, handler)
分别解释一下:
在我们正式介绍 Proxy 之前,建议你对 Reflect 有一定的了解,它也是一个 ES6 新增的全局对象,详细信息请参考 MDN Reflect。
const cat = { color: 'yellow', age: 3, isGirl: true } const handle = { get(target, key, value) { if (key === 'age') { console.log(`I'm ${target[key]}`) } return Reflect.get(target, key, value) }, set(target, key, value) { if (key === 'isGirl') { console.log(`I don't want to be transgender`) return Reflect.set(target, key, `I don't want to be transgender`) } return Reflect.set(target, key, value) } } const newCat = new Proxy(cat, handle) newCat.age // I'm 3 newCat.isGirl = false // I don't want to be transgender newCat.age = 6 console.log(newCat) 复制代码
什么在handler,定义get和set这两个函数名之后就代理对象上的get和set操作了呢? 实际上handler本身就是ES6所新设计的一个对象.它的作用就是用来自定义代理对象的各种可代理操作。它本身一共有13中方法,每种方法都可以代理一种操作.其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() 时。 复制代码
我把把这些方法类似的理解为一些钩子函数,有些方法还是挺好玩的,大家可以仔细研究一下。
比如我们可以把上面的缓存代理换成Proxy的方式来实现:
class Mult { cal() { console.log('开始计算Mult') return [].reduce.call(arguments, ((cur, next) => { return cur*next }), 1) } } const target = new Mult() const newproxy = new Proxy(target.cal, { apply(target, key, value) { target.cache = target.cache || {} let args = [].join.call(value, ',') if (args in target.cache) { return target.cache[args] } return target.cache[args] = target.apply(this, value) } }) console.log(newproxy(1,2,3,4)) console.log(newproxy(1,2,3,4)) console.log(newproxy(1,2,3,4,5)) console.log(newproxy) 复制代码
可以看到也能实现同样的功能,Proxy通过组合使用可以实现各种各样的功能,当然我列出了几种比较常见的
大家可以自己去研究一下,篇幅有限,不再赘述。
如果有不对或者模糊的地方,欢迎大家指正,感谢阅读。