什么是代理模式
代理模式在生活中非常的常见,比如你想卖房子有房产代理人,明星有经纪人可以代理他们的一些事物,外卖小哥也在商家和你之间作为一种代理人,把外卖送到你的手上...
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。 如图:
实现一个简单的设计模式 -李四追女神的故事
四月是春花烂漫的季节,张楚也唱到孤独的人是可耻的,这不在一个明媚的早晨,李四在公园里遇见了让他小鹿乱撞的女孩,他立马就走不动路了,于是他鼓足勇气上去搭讪要微信,迎接他的确是一顿大耳刮子。
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中的适用性都不太高,有兴趣的可以单独去找资料拿来学习。
- 防火墙代理
- 远程代理
- 智能引用代理
- 写时复制代理
Proxy
下面我们来说说ES6新加的这个Api:Proxy,光看名字我们就能想到为什么写代理模式的时候也要讲一下这个Proxy。
在MDN上对于Proxy的解释是:
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
首先它的语法是:
let p = new Proxy(target, handler)
分别解释一下:
- target 是你要代理的对象.它可以是JavaScript中的任何合法对象.如: (数组, 对象, 函数等等)
- handler是你要自定义操作方法的一个集合.
- p 是一个被代理后的新对象,它拥有target的一切属性和方法.只不过其行为和结果是在handler中自定义的,当然它也可以为target添加属性.
在我们正式介绍 Proxy 之前,建议你对 Reflect 有一定的了解,它也是一个 ES6 新增的全局对象,详细信息请参考 MDN Reflect。
Dont BB, Show me Code
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通过组合使用可以实现各种各样的功能,当然我列出了几种比较常见的
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
大家可以自己去研究一下,篇幅有限,不再赘述。
如果有不对或者模糊的地方,欢迎大家指正,感谢阅读。