设计模式

162 阅读6分钟

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。

image-20220206181649135

用ES6模拟一下:

class Singleton {
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

Module

通常的单例对象可能会是下边的样子,暴露几个方法供外界使用。

var Singleton = {
  method1: function () {
    // ...
  },
  method2: function () {
    // ...
  }
};

但如果Singleton 有私有属性,可以写成下边的样子:

var Singleton = {
  privateVar: '我是私有属性',
  method1: function () {
    // ...
  },
  method2: function () {
    // ...
  }
};

但此时外界就可以通过 Singleton 随意修改 privateVar 的值。

为了解决这个问题,我们可以借助闭包,通过 IIFE (Immediately Invoked Function Expression) 将一些属性和方法私有化。

var myInstance = (function() {
  var privateVar = '';
​
  function privateMethod () {
    // ...
  }
​
  return { 
    method1: function () {
    },
    method2: function () {
    }
  };
})();

但随着 ES6 Module 的出现,我们很少像上边那样去定义一个模块了,而是通过单文件,一个文件就是一个模块,同时也可以看成一个单例对象

// 📁 singleton.js
const somePrivateState = []
​
function privateMethod () {
  // ...
}
​
export default {
  method1() {
    // ...
  },
  method2() {
    // ...
  }
}

然后使用的时候 import 即可。

// 📁 main.js
import Singleton from './singleton.js'
// ...

即使有另一个文件也 import 了同一个文件。

// 📁 main2.js
import Singleton from './singleton.js'

由于Module的特性:模块代码仅在第一次导入时被解析,因此这两个不同文件的 importSingleton 是同一个对象。

那如果通过 WebpackES6 转成 ES5 以后呢,这种方式还会是单例对象吗?

答案当然是肯定的,可以看一下 Webpack 打包的产物,其实就是使用了 IIFE ,同时将第一次 import 的模块进行了缓存,第二次 import 的时候会使用之前的缓存。可以看下 __webpack_require__ 的实现,和单例模式的逻辑是一样的。

function __webpack_require__(moduleId) {
  var cachedModule = __webpack_module_cache__[moduleId];
  
  // 单例模式的应用
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
​
  var module = (__webpack_module_cache__[moduleId] = {
    exports: {},
  });
  __webpack_modules__[moduleId](
    module,
    module.exports,
    __webpack_require__
  );
  return module.exports;
}

Mitt 事件总线

export default function mitt<Events extends Record<EventType, unknown>>(
    all?: EventHandlerMap<Events>
): Emitter<Events> {
    type GenericEventHandler =
        | Handler<Events[keyof Events]>
        | WildcardHandler<Events>;
    all = all || new Map();
​
    return {
        all,
​
        on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
            // ......
        },
​
        off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
            // ......
        },
​
        emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
            // ......
    };
}

可以看到它直接将 mitt 这个函数导出了,如果每个页面都各自 import 它,然后通过 mitt() 来生成对象,那发布订阅就乱套了,因为它们不是同一个对象了。

为此,我们可以新建一个模块 / 文件,然后 export 一个实例化对象,其他页面去使用这个对象就实现单例模式了。

import mitt from 'mitt'const emitter = mitt()

实现一个 Storage

实现Storage,使得该对象为单例,基于 localStorage 进行封装。实现方法 setItem(key,value) 和 getItem(key)。

单例模式想要做到的是,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例

要做到这一点,就需要构造函数具备判断自己是否已经创建过一个实例的能力。我们现在把这段判断逻辑写成一个静态方法(其实也可以直接写入构造函数的函数体里):

class Storage {
  static getInstance() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }
  getItem(key) {
    return localStorage.getItem(key);
  }
  setItem(key, value) {
    return localStorage.setItem(key, value);
  }
}

ES5版本:

function StorageBase() {}
​
StorageBase.prototype.getItem = function (key) {
  return localStorage.getItem(key);
};
StorageBase.prototype.setItem = function (key, value) {
  return localStorage.setItem(key, value);
};
​
const Storage = (function () {
  let instance = null;
  return function () {
    if (!instance) {
      instance = new StorageBase();
    }
    return instance;
  };
})();

实现一个全局Modal弹框

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>单例模式弹框</title>
</head>
<style>
    #modal {
        height: 200px;
        width: 200px;
        line-height: 200px;
        position: fixed;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        border: 1px solid black;
        text-align: center;
    }
</style>
<body>
    <button id="open">打开弹框</button>
    <button id="close">关闭弹框</button>
</body>
<script>
    // ES5实现单例模式
    const Modal = (function(){
        let modal = null;
        return function() {
            if(!modal) {
                modal = document.createElement('div');
                modal.innerHTML = '我是一个全局唯一的Modal';
                modal.id = 'modal';
                modal.style.display = 'none';
                document.body.appendChild(modal);
            }
            return modal;
        }
    })()
​
    document.getElementById('open').addEventListener('click', function(){
        const modal = new Modal();
        modal.style.display = 'block';
    })
​
    document.getElementById('close').addEventListener('click', function(){
        const modal = new Modal();
        if (modal) {
            modal.style.display = 'none';
        }
    })
</script>
</html>

迭代器模式

在面向对象编程中,迭代器模式是一种设计模式,其中迭代器用于遍历容器并访问容器中的元素。迭代器模式将算法与容器解耦。

  1. ES6的迭代器是指实现了 Symbol.iterator 方法的对象,当使用 for..of 循环时会调用这个方法返回一个迭代器—— 一个有 next 方法的对象。

    当希望迭代获取下一个元素时,就调用 next() 方法。

    next() 方法返回的结果是 {done: Boolean, value: any},当 done=true 时,表示循环结束。

  2. 通过生成器能使我们能够写出更短的迭代代码。

    执行一个生成器,就得到了一个迭代器。

ES5实现生成器:

function generator(list) {
  let idx = 0;
  let len = list.length;
  return {
    next: function () {
      let done = idx >= len;
      let value = !done ? list[idx++] : undefined;
      return {
        done: done,
        value: value,
      };
    },
  };
}
​
let iterator = generator(["1", "2", "3"]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

策略模式

策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在中国交个人所得税”和“在美国交个人所得税”就有不同的算税方法。

策略模式:

  • 定义了一族算法(业务规则);
  • 封装了每个算法;
  • 这族的算法可互换代替(interchangeable)。

场景

进入一个营销活动页面,会根据后端下发的不同 type ,前端页面展示不同的弹窗。对于这个需求,可以提炼出以下状态映射表。

STYLE_TYPE.Reward - openMoneyPop()
STYLE_TYPE.Poster - openPosterPop()
STYLE_TYPE.Activity - openActivityPop()
STYLE_TYPE.Balance - openBalancePop()
STYLE_TYPE.Cash - openCashBalancePop()

于是我们可以很快的写出以下代码完成需求:

async getMainData() {
  try {
    const res = await activityQuery(); // 请求后端数据
    this.styleType = res.styleType;
    if (this.styleType === STYLE_TYPE.Reward) {
      this.openMoneyPop();
    }else if (this.styleType === STYLE_TYPE.Waitreward) {
      this.openShareMoneyPop();
    } else if (this.styleType === STYLE_TYPE.Poster) {
      this.openPosterPop();
    } else if (this.styleType === STYLE_TYPE.Activity) {
      this.openActivityPop();
    } else if (this.styleType === STYLE_TYPE.Balance) {
      this.openBalancePop();
    } else if (this?.styleType === STYLE_TYPE.Cash) {
      this.openCashBalancePop();
    }
  } catch (error) {
    log.error(MODULENAME, '主接口异常', JSON.stringify(error));
  }
}

我们一起来看看这么写代码会带来什么后果:

  • 首先,它违背了“单一功能”原则。一个 function 里面,处理了四坨逻辑。这样会带来的糟糕后果:比如说万一其中一行代码出了 Bug,那么整个getMainData逻辑都会崩坏;与此同时出了 Bug 你很难定位到底是哪个代码块坏了事;再比如说单个能力很难被抽离复用等等。
  • 不仅如此,它还违背了“开放封闭”原则。即实现“对扩展开放,对修改封闭”的效果。假如未来新增一种弹窗类型的话,我们需要到 getMainData 内部去补一个 else if

接下来采用策略模式优化代码。

我们仔细想想,上面用了这么多 if-else,是不是就是为了把 STYLE_TYPE-弹窗函数 这个映射关系给明确下来?那么在 JS 中,有没有什么既能够既帮我们明确映射关系,同时不破坏代码的灵活性的方法呢?答案就是对象映射

// 📁 popTypes.js
import { SHARETYPE } from './constant';
​
/* 对扩展开放 */
const popTypes = {
  /* 单一功能改造,一个函数对应一个功能 */
  [STYLE_TYPE.Reward]: function() {
    ...
  },
  [STYLE_TYPE.Waitreward]: function() {
    ...
  },
  [STYLE_TYPE.Poster]: function() {
    ...
  },
  [STYLE_TYPE.Activity]: function() {
    ...
  },
  [STYLE_TYPE.Balance]: function() {
    ...
  },
  [STYLE_TYPE.Cash]: function() {
    ...
  },
}
​
/* 对修改封闭 */
export function openPop(type){
  return popTypes[type]();
}
// 📁 main.js
import { openPop } from './popTypes';
​
async getMainData() {
  try {
    const res = await activityQuery(); // 请求后端数据
    openPop(res.styleType);
  } catch (error) {
    log.error(MODULENAME, '主接口异常', JSON.stringify(error));
  }
}

总结

当出现很多 if else 或者 switch 的时候,我们就可以考虑是否能使用策略模式了。

通过策略模式,我们可以把臃肿的代码精简。并实现更好的复用性。

\

参考:

JavaScript 设计模式核⼼原理与应⽤实践

前端的设计模式系列