问题:抽象工厂模式,

工厂模式

工厂模式分为好几种,这里就不一一讲解了,以下是一个简单工厂模式的例子

  1. class Man {
  2. constructor(name) {
  3. this.name = name
  4. }
  5. alertName() {
  6. alert(this.name)
  7. }
  8. }
  9. class Factory {
  10. static create(name) {
  11. return new Man(name)
  12. }
  13. }
  14. Factory.create('yck').alertName()

当然工厂模式并不仅仅是用来 new 出实例
可以想象一个场景。假设有一份很复杂的代码需要用户去调用,但是用户并不关心这些复杂的代码,只需要你提供给我一个接口去调用,用户只负责传递需要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只需要你最后返回我一个实例。这个构造过程就是工厂。
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰。

简单工厂模式

image.pngimage.png
问题—-每考虑到一个新的员工群体,就回去修改一次 Factory 的函数体,这样做糟糕透了——首先,是Factory会变得异常庞大,庞大到你每次添加的时候都不敢下手,生怕自己万一写出一个Bug,就会导致整个Factory的崩坏,进而摧毁整个系统;其次,你坑死了你的队友:Factory 的逻辑过于繁杂和混乱,没人敢维护它;最后,你还连带坑了隔壁的测试同学:你每次新加一个工种,他都不得不对整个Factory 的逻辑进行回归——谁让你的改变是在 Factory 内部原地发生的呢,没有遵守开放封闭原则

我们再复习一下开放封闭原则的内容:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。楼上这波操作错就错在我们不是在拓展,而是在疯狂地修改。

?抽象工厂模式

抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。

抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂
image.png
image.png

单例模式

单例模式很常用,比如全局缓存、全局状态管理等等这些只需要一个对象,就可以使用单例模式。
单例模式的核心就是保证全局只有一个对象可以访问。因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套入 JS 中,我们只需要用一个变量确保实例只创建一次就行,以下是如何实现单例模式的例子

  1. class Singleton {
  2. constructor() {}
  3. }
  4. Singleton.getInstance = (function() {
  5. let instance
  6. return function() {
  7. if (!instance) {
  8. instance = new Singleton()
  9. }
  10. return instance
  11. }
  12. })()
  13. let s1 = Singleton.getInstance()
  14. let s2 = Singleton.getInstance()
  15. console.log(s1 === s2) // true


原型模式

在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在 JavaScript 里,Object.create方法就是原型模式的天然实现——准确地说,只要我们还在借助Prototype来实现对象的创建和原型的继承,那么我们就是在应用原型模式。


适配器模式

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。
以下是如何实现适配器模式的例子

  1. class Plug {
  2. getName() {
  3. return '港版插头'
  4. }
  5. }
  6. class Target {
  7. constructor() {
  8. this.plug = new Plug()
  9. }
  10. getName() {
  11. return this.plug.getName() + ' 适配器转二脚插头'
  12. }
  13. }
  14. let target = new Target()
  15. target.getName() // 港版插头 适配器转二脚插头

装饰模式

装饰模式不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。
装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。

image.png
如此一来,我们就实现了“只添加,不修改”的装饰器模式.

以下是如何实现装饰模式的例子,使用了 ES7 中的装饰器语法

  1. function readonly(target, key, descriptor) {
  2. descriptor.writable = false
  3. return descriptor
  4. }
  5. class Test {
  6. @readonly
  7. name = 'yck'
  8. }
  9. let t = new Test()
  10. t.yck = '111' // 不可修改

在 React 中,装饰模式其实随处可见

装饰器:HOC

image.png

Redux connect

  1. import { connect } from 'react-redux'
  2. class MyComponent extends React.Component {
  3. // ...
  4. }
  5. export default connect(mapStateToProps)(MyComponent)

改写 Redux connect

image.png

代理模式

代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。

事件代理

在实际代码中其实代理的场景很多,也就不举框架中的例子了,比如事件代理就用到了代理模式。

  1. <ul id="ul">
  2. <li>1</li>
  3. <li>2</li>
  4. <li>3</li>
  5. <li>4</li>
  6. <li>5</li>
  7. </ul>
  8. <script>
  9. let ul = document.querySelector('#ul')
  10. ul.addEventListener('click', (event) => {
  11. console.log(event.target);
  12. })
  13. </script>

因为存在太多的 li,不可能每个都去绑定事件。这时候可以通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点。

ES6中的Proxy

image.png
首先,你是不是已经通过了实名审核?如果通过实名审核,那么你可以查看一些相对私密的信息(年龄、职业)。然后,你是不是 VIP ?只有 VIP 可以查看真实照片和联系方式。满足了这两个判定条件,你才可以顺利访问到别人的全部私人信息,不然,就劝退你提醒你去完成认证和VIP购买再来。
image.png

以上主要是 getter 层面的拦截。假设我们还允许会员间互送礼物,每个会员可以告知婚介所自己愿意接受的礼物的价格下限,我们还可以作 setter 层面的拦截。:

image.png

虚拟代理

除了图片懒加载,还有一种操作叫图片预加载。预加载主要是为了避免网络不好、或者图片太大时,页面长时间给用户留白的尴尬。常见的操作是先让这个 img 标签展示一个占位图,然后创建一个 Image 实例,让这个 Image 实例的 src 指向真实的目标图片地址、观察该 Image 实例的加载情况 —— 当其对应的真实图片加载完毕后,即已经有了该图片的缓存内容,再将 DOM 上的 img 元素的 src 指向真实的目标图片地址。此时我们直接去取了目标图片的缓存,所以展示速度会非常快,从占位图到目标图片的时间差会非常小、小到用户注意不到,这样体验就会非常好了。
image.png
这个 PreLoadImage 乍一看没问题,但其实违反了我们设计原则中的单一职责原则PreLoadImage 不仅要负责图片的加载,还要负责 DOM 层面的操作(img 节点的初始化和后续的改变)。这样一来,就出现了两个可能导致这个类发生变化的原因
好的做法是将两个逻辑分离,让 PreLoadImage 专心去做 DOM 层面的事情(真实 DOM 节点的获取、img 节点的链接设置),再找一个对象来专门来帮我们搞加载——这两个对象之间缺个媒婆,这媒婆非代理器不可:

image.png
image.png

缓存代理

缓存代理比较好理解,它应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。

策略模式

image.png—->image.png
说起来你可能不相信,咱们上面的整个重构的过程,就是对策略模式的应用。
现在大家来品品策略模式的定义:

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

状态模式

image.png

image.png—->image.png

发布-订阅模式

发布-订阅模式也叫做观察者模式。通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有很多类似场景,比如我需要在购物网站上购买一个产品,但是发现该产品目前处于缺货状态,这时候我可以点击有货通知的按钮,让网站在产品有货的时候通过短信通知我。
在实际代码中其实发布-订阅模式也很常见,比如我们点击一个按钮触发了点击事件就是使用了该模式

  1. <ul id="ul"></ul>
  2. <script>
  3. let ul = document.querySelector('#ul')
  4. ul.addEventListener('click', (event) => {
  5. console.log(event.target);
  6. })
  7. </script>
  1. class EventEmitter {
  2. constructor() {
  3. // handlers是一个map,用于存储事件与回调之间的对应关系
  4. this.handlers = {}
  5. }
  6. // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  7. on(eventName, cb) {
  8. // 先检查一下目标事件名有没有对应的监听函数队列
  9. if (!this.handlers[eventName]) {
  10. // 如果没有,那么首先初始化一个监听函数队列
  11. this.handlers[eventName] = []
  12. }
  13. // 把回调函数推入目标事件的监听函数队列里去
  14. this.handlers[eventName].push(cb)
  15. }
  16. // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  17. emit(eventName, ...args) {
  18. // 检查目标事件是否有监听函数队列
  19. if (this.handlers[eventName]) {
  20. // 如果有,则逐个调用队列里的回调函数
  21. this.handlers[eventName].forEach((callback) => {
  22. callback(...args)
  23. })
  24. }
  25. }
  26. // 移除某个事件回调队列里的指定回调函数
  27. off(eventName, cb) {
  28. const callbacks = this.handlers[eventName]
  29. const index = callbacks.indexOf(cb)
  30. if (index !== -1) {
  31. callbacks.splice(index, 1)
  32. }
  33. }
  34. // 为事件注册单次监听器
  35. once(eventName, cb) {
  36. // 对回调函数进行包装,使其执行完毕自动被移除
  37. const wrapper = (...args) => {
  38. cb.apply(...args)
  39. this.off(eventName, wrapper)
  40. }
  41. this.on(eventName, wrapper)
  42. }
  43. }

观察者模式与发布-订阅模式的区别

  1. 回到我们上文的例子里。韩梅梅把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员,这种**发布者直接触及到订阅者**的操作,叫观察者模式。但如果韩梅梅没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者,这种**发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式**。<br />![](https://cdn.nlark.com/yuque/0/2020/webp/710497/1595258727511-3e453725-bfc4-4ad0-a1a2-e1b1e8d376cb.webp#align=left&display=inline&height=156&margin=%5Bobject%20Object%5D&originHeight=156&originWidth=158&size=0&status=done&style=none&width=158) ![](https://cdn.nlark.com/yuque/0/2020/webp/710497/1595258730365-5ba15dd3-208f-4b5d-9d53-27d542f70c85.webp#align=left&display=inline&height=185&margin=%5Bobject%20Object%5D&originHeight=185&originWidth=228&size=0&status=done&style=none&width=228)

在我们见过的这些例子里,韩梅梅拉钉钉群的操作,就是典型的观察者模式;而通过EventBus去实现事件监听/发布,则属于发布-订阅模式。

外观模式

外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法

  1. function addEvent(elm, evType, fn, useCapture) {
  2. if (elm.addEventListener) {
  3. elm.addEventListener(evType, fn, useCapture)
  4. return true
  5. } else if (elm.attachEvent) {
  6. var r = elm.attachEvent("on" + evType, fn)
  7. return r
  8. } else {
  9. elm["on" + evType] = fn
  10. }
  11. }

对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可。

迭代器模式

for…of…做的事情,基本等价于下面这通操作:
image.png**
image.png

ES5写一个能够生成迭代器对象的迭代器生成函数

image.png