概述
设计模式的核心思想——封装变化
代码需要考虑可维护性、可扩展性。将变与不变分离,确保变化的部分灵活、不变的部分稳定。
五个基本原则
- 单一功能原则(Single Responsibility Principle)
- 开放封闭原则(Opened Closed Principle)
- 里式替换原则(Liskov Substitution Principle)
- 接口隔离原则(Interface Segregation Principle)
- 依赖反转原则(Dependency Inversion Principle)
23种设计模式
5种 创建型(封装了创建对象过程中的变化)
7种 结构型(封装的是对象之间组合方式的变化,目的在于灵活地表达对象间的配合与依赖关系)
11种 行为型(将对象的行为进行抽离,确保我们能够更安全、更方便地对行为进行更改)
创建型
工厂模式
构造器: 创建对象 确保属性不变,对共性封装;取值操作 开放个性。
unction User(name , age, career, work) {
this.name = name
this.age = age
this.career = career
this.work = work
}
工厂模式其实就是将创建对象的过程单独封装,为了实现无脑传参。
在写了大量构造函数、调用了大量的 new、自觉非常不爽的情况下,我们就应该思考是不是可以掏出工厂模式重构我们的代码了。
function Factory(name, age, career) {
let work
switch(career) {
case 'coder':
work = ['写代码','写系分', '修Bug']
break
case 'product manager':
work = ['订会议室', '写PRD', '催更']
break
case 'boss':
work = ['喝茶', '看报', '见客户']
case 'xxx':
// 其它工种的职责分配
...
return new User(name, age, career, work)
}
抽象工厂模式
开放封闭原则:对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。
和简单工厂模式对比 的共同点,在于都尝试去分离一个系统中变与不变的部分。
不同在于场景的复杂度,抽象工厂本质上处理的其实也是类,但是是一帮非常棘手、繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着千变万化的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,
包含四个关键角色:
- 抽象工厂(抽象类,它不能被用于生成具体实例)
- 具体工厂(用于生成产品族里的一个具体的产品)
- 抽象产品(抽象类,它不能被用于生成具体实例)
- 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品)
class MobilePhoneFactory {
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}
/*OS*/
// 定义操作系统这类产品的抽象产品类
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
class AppleOS extends OS {
controlHardWare() {
console.log('我会用🍎的方式去操作硬件')
}
}
...
/*hardware*/
// 定义手机硬件这类产品的抽象产品类
class HardWare {
// 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
operateByOrder() {
console.log('我会用高通的方式去运转')
}
}
class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}
...
// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()
单例模式
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
class SingleDog {
show() {
console.log('我是一个单例对象')
}
static getInstance() {
// 判断是否已经new过1个实例
if (!SingleDog.instance) {
// 若这个唯一的实例不存在,那么先创建它
SingleDog.instance = new SingleDog()
}
// 如果这个唯一的实例已经存在,则直接返回
return SingleDog.instance
}
}
SingleDog.getInstance = (function() {
// 定义自由变量instance,模拟私有变量
let instance = null
return function() {
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new SingleDog()
}
return instance
}
})()
const s1 = SingleDog.getInstance()
在vuex和redux里的install阶段都有用到单例模式,为了保证一个app只有一个全局store
应用场景: 借助localStorage实现的简单全局唯一store;前端唯一modal 控制显示隐藏(不能变换innerHtml)
原型模式
在 Java 等强类型语言中,原型模式的出现是为了实现类型之间的解耦。
而JavaScript 这门语言的根本就是原型模式。所以暂且不谈设计模式,只谈编程范式。
原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础。在 JavaScript 中,原型编程范式的体现就是基于原型链的继承。这其中,对原型、原型链的理解是关键
Object.create()
deepclone的实现
结构型
装饰器模式
对已有的功能做个拓展,只关心拓展出来的那部分新功能如何实现
ES7 实现的Decorator(target, keyName, descriptor)。@decorator \n function xx(){}
React中的HOC 高阶组件,比如
const BorderHoc = WrappedComponent => class extends Component {
render() {
return <div style={{ border: 'solid 1px red' }}>
<WrappedComponent />
</div>
}
}
Redux connect的 装饰器调用方式,比如
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'
class App extends Component {
render() {
// App的业务逻辑
}
}
function mapStateToProps(state) {
// 假设App的状态对应状态树上的app节点
return state.app
}
function mapDispatchToProps(dispatch) {
// 这段看不懂也没关系,下面会有解释。重点理解connect的调用即可
return bindActionCreators(action, dispatch)
}
// 把App组件与Redux绑在一起
export default connect(mapStateToProps, mapDispatchToProps)(App)
适配器模式
通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题
比如,老的 AjaxRequire函数的调用参数是AjaxRequire(type, url, data, success, failed)
而新的fetch实现是
class FetchUtils{
static get(url){}
static post(url, data){}
}
function AjaxFC(type, url, data, success, failed){}
// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
const type = type.toUpperCase()
let result
try {
// 实际的请求全部由新接口发起
if(type === 'GET') {
result = await HttpUtils.get(url) || {}
} else if(type === 'POST') {
result = await HttpUtils.post(url, data) || {}
}
// 假设请求成功对应的状态码是1
result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
} catch(error) {
// 捕捉网络错误
if(failed){
failed(error.statusCode);
}
}
}
// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
await AjaxAdapter(type, url, data, success, failed)
}
引出了axios
axios 完美地抹平了两种环境下api的调用差异,靠的正是对适配器模式的灵活运用。核心逻辑:派发请求是dispatchRequest
- 数据转换,转换请求体/响应体,可以理解为数据层面的适配;
- 调用适配器。
一个好的适配器的自我修养——把变化留给自己,把统一留给用户。在axios的adapter中,所有关于 http 模块、关于 xhr 的实现细节,全部被 Adapter 封装进了自己复杂的底层逻辑里,暴露给用户的都是十分简单的统一的东西——统一的接口,统一的入参,统一的出参,统一的规则。
代理模式
一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的。
ES6中的Proxy
const proxy = new Proxy(obj, handler)
const girl = {
// 姓名
name: '小美',
// 自我介绍
aboutMe: '...'
// 年龄
age: 24,
// 职业
career: 'teacher',
// 假头像
fakeAvatar: 'xxxx'
// 真实头像
avatar: 'xxxxx',
// 手机号
phone: 123456,
// 礼物数组
presents: [],
// 拒收50块以下的礼物
bottomValue: 50,
// 记录最近一次收到的礼物
lastPresent: present,
}
const baseInfo = ['age', 'career']
const privateInfo = ['avatar', 'phone']// 最私密信息
// 掘金婚介所登场了
const JuejinLovers = new Proxy(girl, {
get: function(girl, key) {
if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
alert('您还没有完成验证哦')
return
}
//...(此处省略其它有的没的各种校验逻辑)
// 此处我们认为只有验证过的用户才可以购买VIP
if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
alert('只有VIP才可以查看该信息哦')
return
}
}
set: function(girl, key, val) {
// 最近一次送来的礼物会尝试赋值给lastPresent字段
if(key === 'lastPresent') {
if(val.value < girl.bottomValue) {
alert('sorry,您的礼物被拒收了')
return
}
// 如果没有拒收,则赋值成功,同时并入presents数组
girl.lastPresent = val
girl.presents = [...girl.presents, val]
}
}
})
前端四种类型的 应用实践
事件代理
div> 10*a
鼠标点击每个 a 标签,都可以弹出“我是第x个a”这样的提示,考虑到事件本身具有“冒泡”的特性,当我们点击 a 元素时,点击事件会“冒泡”到父元素 div 上,从而被监听到father.addEventListener('click', function(e) { if(e.target.tagName==='a'){}}
虚拟代理
缓存代理
应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果
// addAll方法会对你传入的所有参数做求和操作 const addAll = function() { console.log('进行了一次新计算') let result = 0 const len = arguments.length for(let i = 0; i < len; i++) { result += arguments[i] } return result } // 为求和方法创建代理 const proxyAddAll = (function(){ // 求和结果的缓存池 const resultCache = {} return function() { // 将入参转化为一个唯一的入参字符串 const args = Array.prototype.join.call(arguments, ',') // 检查本次入参是否有对应的计算结果 if(args in resultCache) { // 如果有,则返回缓存池里现成的结果 return resultCache[args] } return resultCache[args] = addAll(...arguments) } })()
保护代理
所谓“保护代理”,就是在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。目前实现保护代理时,考虑的首要方案就是 ES6 中的 Proxy
行为型
策略模式
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换,实现算法提取、算法封装、分发优化的整个一条龙的操作流。算法,就是我们这个场景中的询价逻辑,它也可以是你任何一个功能函数的逻辑;“封装”就是把某一功能点对应的逻辑给提出来;“可替换”建立在封装的基础上,只是说这个“替换”的判断过程。
实现“对扩展开放,对修改封闭”的原则。去掉复杂的if-else
复杂交错的多个逻辑杂糅。
对象映射!既能够既帮我们明确映射关系,同时不破坏代码的灵活性
状态模式
策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。
但策略模式中的行为函数是”潇洒“的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。
class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/
// 初始化状态,没有切换任何咖啡模式
this.state = "init";
// 初始化牛奶的存储量
this.leftMilk = "500ml";
}
stateToProcessor = {
that: this,
american() {
console.log("咖啡机现在的牛奶存储量是:", this.that.leftMilk);
console.log("我只吐黑咖啡");
},
latte() {
this.american();
console.log("加点奶");
},
vanillaLatte() {
this.latte();
console.log("再加香草糖浆");
},
mocha() {
this.latte();
console.log("再加巧克力");
},
};
// 关注咖啡机状态切换函数
changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}
const mk = new CoffeeMaker();
mk.changeState("latte");
应用场景是:状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。允许一个对象在其内部状态改变时改变它的行为
观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
// 定义发布者类
class Publisher {
constructor() {
this.observers = []
console.log('Publisher created')
}
// 增加订阅者
add(observer) {
console.log('Publisher.add invoked')
this.observers.push(observer)
}
// 移除订阅者
remove(observer) {
console.log('Publisher.remove invoked')
this.observers.forEach((item, i) => {
if (item === observer) {
this.observers.splice(i, 1)
}
})
}
// 通知所有订阅者
notify() {
console.log('Publisher.notify invoked')
this.observers.forEach((observer) => {
observer.update(this)
})
}
}
// 定义订阅者类
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer.update invoked')
}
}
// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
constructor() {
super()
// 初始化需求文档
this.prdState = null
// 韩梅梅还没有拉群,开发群目前为空
this.observers = []
console.log('PrdPublisher created')
}
// 该方法用于获取当前的prdState
getState() {
console.log('PrdPublisher.getState invoked')
return this.prdState
}
// 该方法用于改变prdState的值
setState(state) {
console.log('PrdPublisher.setState invoked')
// prd的值发生改变
this.prdState = state
// 需求文档变更,立刻通知所有开发者
this.notify()
}
}
// 定义一个具体的订阅者
class DeveloperObserver extends Observer {
constructor() {
super()
// 需求文档一开始还不存在,prd初始为空对象
this.prdState = {}
console.log('DeveloperObserver created')
}
// 重写一个具体的update方法
update(publisher) {
console.log('DeveloperObserver.update invoked')
// 更新需求文档
this.prdState = publisher.getState()
// 调用工作函数
this.work()
}
// work方法,一个专门搬砖的方法
work() {
// 获取需求文档
const prd = this.prdState
// 开始基于需求文档提供的信息搬砖。。。
...
console.log('996 begins...')
}
}
vue双向绑定的原理
有三个关键角色
- observer监听者,不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者。
- watcher订阅者,observer 把数据转发给了真正的订阅者——watcher对象。watcher 接收到新的数据后,会去更新视图。
- compile编译者,MVVM 框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”之类
observer的实现
function observe(target) {
// 若target是一个对象,则遍历它
if(target && typeof target === 'object') {
Object.keys(target).forEach((key)=> {
// defineReactive方法会给目标属性装上“监听器”
defineReactive(target, key, target[key])
})
}
}
// 定义defineReactive方法
function defineReactive(target, key, val) {
const dep = new Dep()
// 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
observe(val)
// 为当前属性安装监听器
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 监听器函数
set: function (value) {
console.log(`${target}属性的${key}属性从${val}值变成了了${value}`)
dep.notify()
val = value
}
});
}
// 定义订阅者类Dep
class Dep {
constructor() {
// 初始化订阅队列
this.subs = []
}
// 增加订阅者
addSub(sub) {
this.subs.push(sub)
}
// 通知订阅者(是不是所有的代码都似曾相识?)
notify() {
this.subs.forEach((sub)=>{
sub.update()
})
}
}
event bus的实现
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
const handlers = this.handlers[eventName].slice()
// 如果有,则逐个调用队列里的回调函数
handlers.forEach((callback) => {
callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
观察者模式和发布-订阅模式的区别
观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者(如图所示)
发布者直接触及到订阅者的操作,叫观察者模式
上述例子是观察者模式
发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。
通过EventBus去实现事件监听/发布,属于发布-订阅模式。
观察者模式没有完全地解决耦合问题——发布者(被观察者)必须去维护一套观察者的集合
迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
Array.prototype.forEach 只是对array数据类型的遍历,对element node array等 类数组不可用。
ES5之前,JS原生的集合类型数据结构,只有Array(数组)和Object(对象);而ES6中,又新增了Map和Set。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套统一的接口机制——迭代器(Iterator)。
ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for…of…循环和迭代器的next方法遍历。 事实上,for…of…的背后正是对next方法的反复调用。
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()