JavaScript设计模式-代理模式

172 阅读3分钟

代理模式

代理模式是一个对象不能直接访问另一个对象,需要一个第三者代理从而间接达到访问目的。

1、第一个例子

class RealImg {
  constructor(fileName) {
    this.fileName = fileName
    this.loadFromDisk()
  }
  display() {
    console.log('display...' + this.fileName)
  }
  loadFromDisk() {
    console.log('loading...' + this.fileName)
  }
}

class ProxyImg {
  constructor(fileName) {
    this.realImg = new RealImg(fileName)
  }
  display() {
    this.realImg.display()
  }
}

let proxyImg = new ProxyImg('1.jpg')
proxyImg.display()

上面的代码中,ProxyImgRealImg 做了代理,访问者不能直接访问 RealImg,只能通过 ProxyImg 对目标对象间接访问,这样子对目标进行了保护。

2、保护代理

当用户需要访问主体 A 时,通过代理 B 可以过滤掉一些请求,比如一些私密的信息不能被别人访问,这种请求可以直接在代理 B 这里过滤掉,这种代理叫作保护代理

保护代理在上面已经举过例子了,这里就不重复演示了。

保护代理用于控制不同权限的对象对目标对象的访问。

3、虚拟代理

虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。虚拟代理是最常用的一种代理模式。下面通过一个例子来演示。

3.1、虚拟代理实现图片预加载

图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法就是先用一张 loading 图片占位,然后用异步方式加载图片,等图片加载好了再把它填充到 img 节点中,这种场景就很适合使用虚拟代理。

假如没有使用代理对象,创建一个普通的本体对象,如下所示:

var myImage = (function () {
  var imgNode = document.createElement('img')
  document.body.appendChild(imgNode)

  return {
    setImgSrc: function (src) {
      imgNode.src = src
    },
  }
})()

myImage.setImgSrc('https://xxxx.com/xxx.jpg')

当网速很慢的时候,通过 myImage.setSrc 给该 img 节点设置 src,在图片加载好之前,有一段很长的时间都是空白的。

现在加入代理对象,通过代理对象,在图片被真正加载好之前,页面中显示 loading 图占位。代码如下:

var myImage = (function () {
  var imgNode = document.createElement('img')
  document.body.appendChild(imgNode)

  return {
    setImgSrc: function (src) {
      imgNode.src = src
    },
  }
})()

var proxyImg = (function () {
  var image = new Image()
  image.onload = function () {
    myImage.setImgSrc(this.src)
  }

  return {
    setImgSrc: function (src) {
      myImage.setImgSrc('https://xxxx.com/loading.jpg')
      image.src = src
    },
  }
})()

proxyImg.setImgSrc('https://xxxx.com/xxx.jpg')

上面的代码通过 proxyImg 间接地访问 myImage。proxyImg 控制了用户对 myImage 的访问,并在次此过程中添加了一些额外的操作,比如图片加载好之前,添加 loading 图片占位。

4、事件代理

事件代理的场景是一个父元素下有多个子元素:代码如下:

<div id="father">
  <a href="#">链接1</a>
  <a href="#">链接2</a>
  <a href="#">链接3</a>
  <a href="#">链接4</a>
  <a href="#">链接5</a>
  <a href="#">链接6</a>
</div>

现在需要实现点击每个 a 标签,都打印出对应的文字。当不使用代理模式的时候,我们自然能想到给每个 a 绑定点击事件,如下代码所示:

var allA = document.querySelectorAll('a')
for (var i = 0; i < allA.length; i++) {
  ;(function (i) {
    allA[i].addEventListener('click', function (e) {
      e.preventDefault()
      console.log(allA[i].innerText)
    })
  })(i)
}

然后由于事件会冒泡,点击 a 标签,会冒泡到父元素上,所以只需要在 div 上绑定一次即可,而不需要给每个 a 绑定事件,这就是事件代理。代码如下:

var father = document.querySelector('#father')
father.addEventListener('click', function (e) {
  if (e.target.tagName === 'A') {
    e.preventDefault()
    console.log(e.target.innerText)
  }
})

上面的代码中,点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素。

5、缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

5.1、缓存代理的例子——计算乘积

首先需要一个计算乘积的函数:

function mult() {
  console.log('新一次的计算')
  let a = 1
  for (let i = 0; i < arguments.length; i++) {
    a = a * arguments[i]
  }
  return a
}

mult(2, 3, 4) // 新一次的计算 24
mult(2, 3, 4) // 新一次的计算 24

可以从上面看出,在没有加入缓存代理之前,对于相同的参数,也会重新计算一次。

现在加入缓存代理函数,代码如下:

function mult() {
  console.log('新一次的计算')
  let a = 1
  for (let i = 0; i < arguments.length; i++) {
    a = a * arguments[i]
  }
  return a
}

var proxyMult = (function () {
  var cache = {}
  return function () {
    var args = Array.prototype.join.call(arguments, ',')
    if (args in cache) {
      return cache[args]
    }
    return (cache[args] = mult.apply(this, arguments))
  }
})()

mult(2, 3, 4) // 新一次的计算 24
mult(2, 3, 4) // 24

从上面的代码可以看出,proxyMult 针对重复的入参只会计算一次,这将大大节省计算过程中的时间开销。通过增加缓存代理的方式,mult 函数可以继续专注于自身的职责,而缓存的功能是由代理对象实现的。

6、总结

6.1、代理的意义

通过代理对象,实际上给系统添加了新的行为,但是并没有改变本体的接口。拿图片预加载的例子来说,给 img 节点设置 src 和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。这是符合开放-封闭原则的。

6.2 代理模式与适配器模式、装饰者模式的对比

  • 代理模式 VS 适配器模式

    • 适配器模式:提供一个不同的接口(比如转接头)
    • 代理模式:提供一个一模一样的接口
  • 代理模式 VS 装饰者模式

    • 装饰者模式:扩展功能,原有功能不变且可直接使用
    • 代理模式:显示原有的功能,但是经过代理限制之后

值得一提的是,ES6 中的 Proxy,它本身就是为拦截而生的,所以目前实现保护代理时,考虑的首要方案就是 ES6 中的 Proxy。关于 Proxy 的更多内容,可以查看我的另一篇文章ES6 中的 Proxy 学习心得