原则
- 单一功能
- 开放封闭
- 里式替换
- 接口隔离
- 依赖反转
开发中主要用单一功能和开放封闭原则
核心:将变化造成影响最小化,变与不变分离,保证变化部分灵活,不变部分稳定。场景是基础,代码是辅助,逻辑是主角。
不同的设计模式用自己的方式封装不同类型的变化 ——
创建型:封装创建对象过程中的变化。工厂模式:将创建对象的过程抽离;
结构型:对象间组合方式的变化,灵活表达对象间配合与依赖关系;
行为型:对象行为抽离,确保安全方便地对行为更改。
构造器模式
就是ES5构造函数,new一个实例,传不同的数据,实例化不同的对象。new的过程变的是value,不变的是key。构造器将 name、age、career 赋值给对象的过程封装,确保每个对象都具备这些key,不变的地方稳定。同时将 name、age、career 的取值开放,确保变化部分灵活。本质是抽象每个实例的变与不变。
场景:创建不同的用户,每个用户具备name、age、caree属性。
解决:实例之间。
简单工厂模式
抽象不同构造函数(类)之间的变与不变。new之前先判断变化的部分,确定了再传参。
场景:不同工种分配不同的职责说明,每个工种用户加个性化字段,分配构造器之前判断工种。不同的构造器中key相同,但是key1决定key2的变化。
解决:类之间的问题,将创建对象的过程单独封装。有构造函数的地方就有简单工厂。
抽象工厂模式
ps:开放封闭原则:拓展开放,修改封闭。软件实体(类、模块、函数)可扩展,但不可修改。
场景:简单工厂直接传参,但是不同参数new出来的对象权限及功能不同,需要对某个对象进行单独逻辑处理,例如在case为boss的地方逻辑处理,这样每增加一个群体就在在Factory里面修改不符合开放封闭原则。所以出现了抽象工厂。
class MobilePhoneFactory {
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
// 具体产品类
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
// 具体产品类
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}
// 定义操作系统这类产品的抽象产品类
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
class AppleOS extends OS {
controlHardWare() {
console.log('我会用🍎的方式去操作硬件')
}
}
// 定义手机硬件这类产品的抽象产品类
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 newStarFactory extends MobilePhoneFactory {
createOS() {
// 操作系统实现代码
}
createHardWare() {
// 硬件实现代码
}
}
MobilePhoneFactory是最顶层的抽象工厂,里面包含多个子工厂,抽象工厂不能new实例,实际new实例的是子工厂,子工厂可以自由组合形成一个具体工厂FakeStarFactory,它继承抽象工厂。
class类,抽象工厂里面提供接口抽象不同的类,不能生成具体实例。具体工厂继承extends抽象工厂, 自定义实例,后续扩展时直接extends抽象工厂就可以,不会对其他造成影响。
- 抽象工厂(抽象类,不能生成具体实例): 声明目标产品的共性。在一个系统里,抽象工厂可以有多个(手机厂被更大的厂收购,这个厂除了手机抽象类,还有平板、游戏机抽象类等),每个抽象工厂对应的这一类的产品,称为“产品族”。
- 具体工厂(生成产品族里具体的产品): 继承自抽象工厂、实现抽象工厂里声明的方法,用于创建具体产品类。
- 抽象产品(抽象类,不能生成具体实例): 具体工厂里实现的接口,会依赖一些类,这些类对应到各种具体的细粒度产品(操作系统、硬件),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
- 具体产品(生成产品族里具体的产品所依赖的更细粒度的产品): 比如具体的一种操作系统、或具体的一种硬件。
解决:工厂与工厂之间
单例模式
概念:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
实现思路:如何保证一个类仅有一个实例?创建一个类(构造函数),new生成任意多的实例对象。不同对象各占一块内存空间,相互独立。单例模式想实现的是,不管创建多少次,只返回第一次创建的唯一实例。所以构造函数要判断自己是否创建过一个实例。
class SingleDog {
show() {
console.log('我是一个单例对象')
}
static getInstance() {
// 判断是否已经new过1个实例
if (!SingleDog.instance) {
// 若这个唯一的实例不存在,那么先创建它
SingleDog.instance = new SingleDog()
}
// 如果这个唯一的实例已经存在,则直接返回
return SingleDog.instance
}
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()
// true
s1 === s2
场景:vuex
Vue组件间通信常用的是 props(父子组件),事件监听(兄弟组件)。组件复杂时将共享的数据抽离放在全局,供组件们按一定的的规则存取数据,保证状态以一种可预测的方式变化。于是有了 Vuex存放共享数据的唯一数据源Store。一个 Vue 实例只能对应一个 Store。
Vuex如何确保Store的唯一性?
Vuex 插件对象内部实现了 install 方法,它会在插件安装时被调用,从而把 Store 注入到Vue实例里。每 install 一次,都会给 Vue 实例注入一个 Store。在 install 方法里:保证一个 Vue 实例只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会有一个全局的 Store。
let Vue // 这个Vue的作用和楼上的instance作用一样
export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}
实现一个Storage
实现Storage,使该对象为单例,基于 localStorage 封装。实现方法 setItem(key,value) 和 getItem(key)。
思路:getInstance方法和instance变量。判断逻辑写入静态方法或者构造函数里或用闭包。
静态方法
// 定义Storage
class Storage {
static getInstance() {
// 判断是否已经new过1个实例
if (!Storage.instance) {
// 若这个唯一的实例不存在,那么先创建它
Storage.instance = new Storage()
}
// 如果这个唯一的实例已经存在,则直接返回
return Storage.instance
}
getItem (key) {
return localStorage.getItem(key)
}
setItem (key, value) {
return localStorage.setItem(key, value)
}
}
const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()
storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')
// 返回true
storage1 === storage2
闭包
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
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(){
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new StorageBase()
}
return instance
}
})()
// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果
const storage1 = new Storage()
const storage2 = new Storage()
storage1.setItem('name', '李雷')
// 李雷
storage1.getItem('name')
// 也是李雷
storage2.getItem('name')
// 返回true
storage1 === storage2
全局的模态框
<!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>
// 核心逻辑,这里采用了闭包思路来实现单例模式
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() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
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>
原型模式
概述
JS本质就是原型模式。强类型语言中,原型模式是为了实现类型之间解耦。而 JS 本身类型比较模糊,不存在类型耦合问题,所以平时不会刻意使用原型模式。不必强行把原型模式当作设计模式理解,作为一种编程范式更合适。原型模式是设计模式也是一种编程范式,是 JS面向对象系统实现的根基。
原型模式下,创建一个对象,先找一个对象作为原型,然后克隆原型来创建出一个与原型一样(共享一套数据/方法)的对象。Object.create就是原型模式,凡是借助Prototype实现对象创建和原型继承,就是在应用原型模式。JS中原型模式,不是为了得到副本,而是为了得到与构造函数(类)相对应的类型的实例、实现数据/方法的共享。克隆是实现的方法,但克隆本身不是目的。
Java中类是面向对象的根基,创建两个一样的实例需要new两次传同样的参数,JAVA中针对原型模式设计了一套接口和方法,必要场景下会通过原型方法来应用原型模式。更多情况下,J仍以“实例化类”方式创建对象。所以在以类为中心的语言中,原型模式不是必选项,只在特定场景下用。
JS的es6语法也支持类,但是这里的类其实是原型继承的语法糖。下面两段代码等价:
class Dog {
constructor(name ,age) {
this.name = name
this.age = age
}
eat() {
console.log('肉骨头真好吃')
}
}
function Dog(name, age) {
this.name = name
this.age = age
}
Dog.prototype.eat = function() {
console.log('肉骨头真好吃')
}
原型编程范式
核心思想是利用实例描述对象,用实例作为定义对象和继承的基础。JS 中,原型编程范式的体现就是基于原型链继承。对原型、原型链理解是关键。
原型
JS中,每个构造函数都有一个prototype属性,它指向构造函数的原型对象,这个原型对象中有个 constructor 属性指回构造函数;每个实例都有个__proto__属性,当用构造函数创建实例时,实例的__proto__属性会指向构造函数的原型对象。 具体来说,当我们这样使用构造函数创建一个对象时:
// 创建一个Dog构造函数
function Dog(name, age) {
this.name = name
this.age = age
}
Dog.prototype.eat = function() {
console.log('肉骨头真好吃')
}
// 使用Dog构造函数创建dog实例
const dog = new Dog('旺财', 3)
原型链
上面代码基础上,进行两个方法调用:
// 输出"肉骨头真好吃"
dog.eat()
// 输出"[object Object]"
dog.toString()
没有在 dog 实例里定义 eat 和 toString,还是被成功调用。是因为访问实例的属性/方法时,首先搜索这个实例本身;若实例没有定义对应的属性/方法,会去搜索实例的原型对象;若原型对象中也搜不到,就搜索原型对象的原型对象,这个搜索的轨迹,叫做原型链。
eat 和 toString 调用搜索过程:
这些彼此相连的prototype,就组成一个原型链。 注: 所有 JS 中对象都是原型链顶端的 Object 的实例,除了Object.prototype(手动Object.create(null)创建一个没有任何原型的对象,就不是 Object 的实例)。
ps:混淆 JS 中原型范式和强类型语言中原型模式区别,可能是为了考察对象的深拷贝。
模拟 JAVA 中的克隆接口?
JavaScript 实现原型模式?
实现JS中的深拷贝?
对象深拷贝
方法一 JSON.stringify
const liLei = {
name: 'lilei',
age: 28,
habits: ['coding', 'hiking', 'running']
}
const liLeiStr = JSON.stringify(liLei)
const liLeiCopy = JSON.parse(liLeiStr)
liLeiCopy.habits.splice(0, 1)
console.log('李雷副本的habits数组是', liLeiCopy.habits)
console.log('李雷的habits数组是', liLei.habits)
引用类型也被成功拷贝,副本和本体互不干扰。这个方法无法处理 function、正则等——只有当对象是个严格的 JSON 对象时,可以用这个方法。
方法二 递归
若属性为值类型,直接返回;为引用类型,递归遍历
function deepClone(obj) {
// 是 值类型 或 null,则直接return
if(typeof obj !== 'object' || obj === null) {
return obj
}
// 定义结果对象
let copy = {}
// 如果对象是数组,则定义结果数组
if(obj.constructor === Array) {
copy = []
}
// 遍历对象的key
for(let key in obj) {
// 如果key是对象的自有属性
if(obj.hasOwnProperty(key)) {
// 递归调用深拷贝方法
copy[key] = deepClone(obj[key])
}
}
return copy
}
装饰器模式
功能分离,通用基础功能上添加新的自定义逻辑。
es7中定义一个装饰器,通过@装饰器名称 给class添加装饰器。
// 装饰器函数,它的第一个参数是目标类
function classDecorator(target) {
target.hasDecorator = true
return target
}
// 将装饰器“安装”到Button类上
@classDecorator
class Button {
// Button类的相关逻辑
}
// 验证装饰器是否生效
console.log('Button 是否被装饰了:', Button.hasDecorator)
语法糖
// 具体的参数意义,在下个小节,这里大家先感知一下操作
function funcDecorator(target, name, descriptor) {
let originalMethod = descriptor.value
descriptor.value = function() {
console.log('我是Func的装饰器逻辑')
return originalMethod.apply(this, arguments)
}
return descriptor
}
class Button {
@funcDecorator
onClick() {
console.log('我是Func的原有逻辑')
}
}
// 验证装饰器是否生效
const button = new Button()
button.onClick()
@decorator作用
- 函数传参 调用装饰器函数,将被装饰者“交给”装饰器
- 定义装饰器函数,将“属性描述对象”交到你手里
适配器模式
编写适配器函数,用适配器承接旧接口的参数,可以实现新旧接口无缝衔接。
代理模式
vpn访问谷歌就是代理模式
ES6的proxy
const proxy = new Proxy(obj, handler)
第一个参数是目标对象,handler 也是对象,定义代理行为。通过 proxy 访问目标对象的,handler会拦截,每次访问都要经过 handler 第三方。可以定义getter和setter之前拦截。
事件代理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>事件代理</title>
</head>
<body>
<div id="father">
<a href="#">链接1号</a>
<a href="#">链接2号</a>
<a href="#">链接3号</a>
<a href="#">链接4号</a>
<a href="#">链接5号</a>
<a href="#">链接6号</a>
</div>
</body>
</html>
点击每个 a 标签,都弹出“我是xxx”。意味着至少安装 6 个监听函数给 6 个不同的元素(循环),a 标签增多,性能开销大。考虑事件“冒泡”特性,点击 a 元素,事件会“冒泡”到父元素,从而被监听。那么事件监听函数只需在 div 元素上被绑定一次,不需要在子元素上被绑定 N 次——事件代理。
// 获取父元素
const father = document.getElementById('father')
// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
// 识别是否是目标子元素
if(e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
} )
虚拟代理
图片预加载:先让 img 标签展示占位图,然后创建 Image 实例,让实例的 src 指向真实目标图片地址、观察该实例加载情况 —— 当其对应的真实图片加载完后,即已经有该图片的缓存内容,再将 DOM 上的 img 元素的 src 指向真实的目标图片地址。此时直接取了目标图片的缓存,所以展示速度非常快,从占位图到目标图片的时间差小到用户注意不到。
new Image()是实现虚拟代理的关键
class PreLoadImage {
constructor(imgNode) {
// 获取真实的DOM节点
this.imgNode = imgNode
}
// 操作img节点的src属性
setSrc(imgUrl) {
this.imgNode.src = imgUrl
}
}
class ProxyImage {
// 占位图的url地址
static LOADING_URL = 'xxxxxx'
constructor(targetImage) {
// 目标Image,即PreLoadImage实例
this.targetImage = targetImage
}
// 该方法主要操作虚拟Image,完成加载
setSrc(targetUrl) {
// 真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(ProxyImage.LOADING_URL)
// 创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image()
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
this.targetImage.setSrc(targetUrl)
}
// 设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl
}
}
缓存代理
应用于计算量大的场景,用空间换时间——当需要用某个已经计算过的值的时候,不再二次计算,从内存取现成的结果。这种场景下,需要代理帮我们在计算的同时,将计算结果缓存。比如:对传入的参数求和,柯里化
// 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)
}
})()
保护代理
proxy实现,在 getter 和 setter 函数里校验和拦截,确保变量安全。
策略模式
遵循单一功能原则,扩展开放修改封闭,做对象映射。只需关注计算逻辑本身。
状态模式
遵循单一功能原则,扩展开放修改封闭,做对象映射。但是计算逻辑本身需要对主体有感知。状态-行为映射对象作为主体类对应实例的一个属性添加进去:通过this共享信息
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')
}
}
应用场景:vue双向数据绑定,组件通信
迭代器模式
ES6中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。原理都一样。任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,本质是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后是对next方法反复调用。
ES6实现迭代器生成函数
// 编写一个迭代器生成函数
function *iteratorGenerator() {
yield '1号选手'
yield '2号选手'
yield '3号选手'
}
const iterator = iteratorGenerator()
iterator.next()
iterator.next()
iterator.next()
ES5实现迭代器生成函数
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {
// idx记录当前访问的索引
var idx = 0
// len记录传入集合的长度
var len = list.length
return {
// 自定义next方法
next: function() {
// 如果索引还没有超出集合长度,done为false
var done = idx >= len
// 如果done为false,则可以继续取值
var value = !done ? list[idx++] : undefined
// 将当前值与遍历是否完毕(done)返回
return {
done: done,
value: value
}
}
}
}
var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()