这是我参与「 第四届青训营 」笔记创作活动的第二天
23种设计模式
创建型 - 如何创建一个对象
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型 - 如何灵活的将对象组装成较大的结构
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
行为型 - 负责对象间的高效通信和职责划分
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
设计模式的核心是‘观察整个逻辑里面的变与不变,然后将变与不变分离,达到使变化的部分灵活、不变的地方稳定的目的’
浏览器中的设计模式
- 单例模式
- 发布订阅模式
单例模式
定义:全局唯一访问对象(一般登录、购物车等都是一个单例)
应用场景:缓存,全局状态管理、浏览器的 window 对象等
作用:
- 模块间通信
- 保证某个类的对象的唯一性
- 防止变量污染
注意:
- 正确使用 this
- 闭包容易造成内存泄漏,需及时清除不需要的变量
- 创建一个新对象的成本较高
// 单例对象
class SingleObject {
login () {}
}
// 访问方法
SingleObject.getInstance = (function () {
let instance;
return function () {
if (!instance) {
instance = new SingleObject();
}
return instance;
}
})()
const obj1 = SingleObject.getInstance();
const obj2 = SingleObject.getInstance();
console.log(obj1 === obj2); // true
//可以通过 SingleObject.getInstance() 来获取到单例,并且每次调用均获得到同一个单例
发布订阅模式(≠观察者模式)
定义:一种订阅机制,可在被订阅对象发生变化时通知订阅者
消息的发布者,不会将消息直接发送给特定的订阅者,而是通过消息通道广播出去,订阅者通过订阅获取到想要的消息。
应用场景:从系统架构之间的解耦,到业务中的一些实现模式,像邮件订阅,上线订阅,系统消息通知,网站日志记录,JavaScript 事件机制,react/vue 等的观察者等等
使用条件:
- 各模块相互独立
- 存在一对多的依赖关系
- 依赖模块不稳定、依赖关系不稳定(避免对象间的紧密耦合)
- 各模块由不同的人员、团队开发
注意:要先监听,再触发
Javascript 中的设计模式
- 工厂模式
- 适配器模式
- 装饰器模式
- 原型模式
- 代理模式
- 外观模式
- 迭代器模式
- 策略模式
工厂模式
定义:相当于创建实例对象的 new,提供一个创建对象的接口
应用场景:JQuery 中的$、Vue.component 异步组件、React.createElement 等
- 对象的构建十分复杂
- 需要依赖具体环境创建不同实例
- 处理大量具有相同属性的小对象
// 某个需要创建的具体对象
class Product {
constructor (name) {
this.name = name;
}
init () {}
}
// 工厂对象
class Creator {
create (name) {
return new Product(name);
}
}
const creator = new Creator();
const p = creator.create(); // 通过工厂对象创建出来的具体对象
适配器模式
定义:用来解决两个接口不兼容的问题,由一个对象来包装不兼容的对象(比如参数转换,允许直接访问)
应用场景:Vue 中的 computed、旧的 JSON 格式转换成新的格式等
装饰器模式
定义:在不改变对象自身的基础上,动态的给某个对象添加新的功能,同时又不改变其接口
应用场景:ES7 装饰器、Vuex 中1.0版本混入 Vue 时,重写 init 方法、Vue 中数组变异方法实现等
原型模式
定义:复制已有对象来创建新的对象
应用场景:JS 中对象创建的基本模式
代理模式
定义:可自定义控制对原对象的访问方式,并且允许在更新前后做一些额外处理(为其他对象提供一种代理,便以控制对这个对象的访问,不能直接访问目标对象)
应用场景:监控,代理工具,前端框架实现,ES6 Proxy,Vuex 中对于 getters 访问、图片预加载等等
作用:
- 远程代理(一个对象对另一个对象的局部代理)
- 虚拟代理(对于需要创建开销很大的对象如渲染网页大图时,可以先用缩略图代替真图)
- 安全代理(保护真实对象的访问权限)
- 缓存代理(一些开销较大的运算提供暂时的存储,下次运算时,如果传递进来的参数跟之前的相同,则可以直接返回前面存储的运算结果)
// 缓存代理
function sum(a, b){
return a + b
}
let proxySum = (function(){
let cache = {}
return function(){
let args = Array.prototype.join.call(arguments, ',');
if(args in cache){
return cache[args];
}
cache[args] = sum.apply(this, arguments)
return cache[args]
}
})()
外观模式
定义:为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易,不符合单一职责和开放封闭原则(一个函数封装了复杂的操作)
应用场景:JS 事件不同浏览器兼容处理、同一方法可以传入不同参数兼容处理等
作用:
- 对接口和调用者进行了一定的解耦
- 创造经典的三层结构 MVC
- 在开发阶段减少不同子系统之间的依赖和耦合,方便各个子系统的迭代和扩展
- 为大型复杂系统提供一个清晰的接口
注意:外观模式被开发者连续调用时会造成一定的性能损耗(由于每次调用都会进行可用性检测)
//兼容不同浏览器的事件监听函数
function on(type, fn){
// 对于支持dom2级事件处理程序
if(document.addEventListener){
dom.addEventListener(type,fn,false);
}else if(dom.attachEvent){
// 对于IE9一下的ie浏览器
dom.attachEvent('on'+type,fn);
}else {
dom['on'+ type] = fn;
}
}
迭代器模式
定义:在不暴露数据类型的情况下访问集合中的数据(提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示)
应用场景:数据结构中有多种数据类型,列表,树等,提供通用操作接口(forEach、map、reduce等)
作用:
- 为遍历不同集合提供统一的接口
- 保护原集合但又提供外部访问内部元素的方式
//遍历函数(不仅可以遍历数组和字符串,还可以遍历对象)
function _each(el, fn = (v, k, el) => {}) {
// 判断数据类型
function checkType(target){
return Object.prototype.toString.call(target).slice(8,-1)
}
// 数组或者字符串
if(['Array', 'String'].indexOf(checkType(el)) > -1) {
for(let i=0, len = el.length; i< len; i++) {
fn(el[i], i, el)
}
}else if(checkType(el) === 'Object') {
for(let key in el) {
fn(el[key], key, el)
}
}
}
策略模式
定义:将不同算法进行合理的分类和单独封装,让不同算法之间可以互相替换而不会影响到算法的使用者
应用场景:实现更优雅的表单验证,游戏里的角色计分器,棋牌类游戏的输赢算法,微信抢红包算法等
作用:
- 实现不同,作用一致
- 调用方法相同,降低了使用成本以及不同算法之间的耦合
- 单独定义算法模型,方便单元测试
- 避免大量冗余的代码判断(比如 if else)
//根据不同类型实现求和算法
const obj = {
A: (num) => num * 4,
B: (num) => num * 6,
C: (num) => num * 8
}
const getSum =function(type, num) {
return obj[type](num)
}
前端框架中的设计模式
- 代理模式
- 组合模式
代理模式
组合模式
定义:可多个对象组合使用,也可单个对象独立使用
应用场景:DOM,前端组件,文件目录,部门