单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。
用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的特性:模块代码仅在第一次导入时被解析,因此这两个不同文件的 import的Singleton 是同一个对象。
那如果通过 Webpack 将 ES6 转成 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>
迭代器模式
在面向对象编程中,迭代器模式是一种设计模式,其中迭代器用于遍历容器并访问容器中的元素。迭代器模式将算法与容器解耦。
-
ES6的迭代器是指实现了 Symbol.iterator 方法的对象,当使用 for..of 循环时会调用这个方法返回一个迭代器—— 一个有 next 方法的对象。
当希望迭代获取下一个元素时,就调用
next()方法。next()方法返回的结果是{done: Boolean, value: any},当done=true时,表示循环结束。 -
通过生成器能使我们能够写出更短的迭代代码。
执行一个生成器,就得到了一个迭代器。
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 的时候,我们就可以考虑是否能使用策略模式了。
通过策略模式,我们可以把臃肿的代码精简。并实现更好的复用性。
\
参考: