携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情 >>
本篇博文源于阅读JavaScript 设计模式核⼼原理与应⽤实践的总结
SOLID设计原则
- 单一功能原则(Single Responsibility Principle)
- 开放封闭原则(Opened Closed Principle)
- 里氏替换原则(Liskov Substitution Principle)
- 接口隔离原则(Interface Segregation Principle)
- 依赖反转原则(Dependency Inversion Principle)
单例模式
实现一个Storage
描述:
实现Storage,使得该对象为单例,基于localStorage进行封装。实现方法setItem(key,value)和getItem(key)
实现:
// 先实现一个基础的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
实现一个全局的模态框
描述:
实现一个全局唯一的Modal弹框
实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>单例模式弹框</title>
<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>
</head>
<body>
<button id="open">打开弹框</button>
<button id="close">关闭弹框</button>
<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的形式调用也可以,和storeage同理
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>
</body>
</html>
\
原型模式
对象的深拷贝
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
}
\
装饰器模式
React中的装饰器:HOC
编写一个高阶组件,它的作用是把传入的组件丢进一个有红色边框的容器里(拓展其样式)
import React,{Component} from 'react'
const BorderHOC = WrappedComponent => class extends Component{
render(){
return <div style={{border: 'solid 1px red'}}>
<WrappedComponent />
</div>
}
}
使用:
import React,{Component} from 'react'
import BorderHOC from './BorderHOC'
// 用BorderHOC装饰目标组件
@BorderHOC
class TargetComponent extends React.Component{
render(){
// 目标组件具体的业务逻辑
}
}
// export出去的其实是一个被包裹后的组件
export default TargetComponent
\
代理模式
使用代理模式建立一个婚介所
// 规定礼物的数据结构有type和value组成
const present = {
type: '巧克力',
value: 60
}
// 为用户增开presents字段存储礼物
const girl={
// 姓名
name:'小美',
// 自我介绍
aboutMe:'...',
// 年龄
age:24,
// 职业
career:'teacher',
// 假头像
fakeAvatar: 'xxx',
// 真实头像
avatar: 'xxx',
// 手机号
phone:12345,
// 礼物数组
presents:[],
// 拒收50元以下的礼物
bottomValue:50,
// 记录最近一次收到的礼物
lastPresent:present,
}
// 婚介所开张了
const Lovers=new Proxy(girl,{
get:function(girl,key){
if(baseInfo.indexOf(key)!==-1&&!user.isValidated){
alert('你还没有完成验证')
return
}
// 此处我们认为只有验证过的用户才可以购买VIP
if(user.isValidated&&privateInfo.indexOf(key)!==-1&&!user.isVIP){
alter('只有VIP才可以查看该信息')
return
}
},
set:function(girl,key,val){
// 最近一次送来的礼物会尝试给lastPresent字段
if(key==='lastPresent'){
if(val.value<girl.bottomValue){
alert('sorry,您的礼物被拒收了')
return
}
// 如果没有被拒收,则赋值成功,同时并入present数组
girl.lastPresent=val
girl.presents=[...girl.presents,val]
}
}
}
})
\
事件代理
一个父元素下有多个子元素,需要为每个子元素监听点击事件
<!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>
const father=document.getElementById('father')
// 给父元素安装一次监听函数
father.addEventListener('click',function(e){
// 识别是否是目标子元素
if(e.target.tagName==='A'){
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
})
\
虚拟代理
图片的预加载:为了避免网络不好、或者图片太大时,页面长时间给用户留白的尴尬,常见的操作是先让这个img标签展示一个占位图,然后创建一个Image实例,让这个Image实例的src指向真实的目标图片地址、观察该Image实例的加载情况——当其对应的真实图片加载完毕后,即已经有了该图片的缓存内容,再将DOM上的img元素的src指向真实的目标图片地址。此时我们直接去取了目标图片的缓存,所以展示速度会非常快,从占位图到目标图片的时间差会非常小、小到用户注意不到,这样体验就会非常好了
class PreLoadImage{
constructor(imgNode){
// 获取真实的DOM节点
this.imgNode=imgNode
}
// 操作img节点的src属性
setSrc(imgUrl){
this.imgNode.src=imgUrl
}
}
class ProxyImage{
// 占位图的url地址
static LOADING_URL='xxx'
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)
}
})()
\
保护代理
目前实现保护代理时,考虑的首要方案就是ES6中的Proxy
策略模式
if-else策略改造
// 询价方法,接受价格标签和原价为入参
function askPrice(tag, originPrice) {
// 处理预热价
if(tag === 'pre') {
if(originPrice >= 100) {
return originPrice - 20
}
return originPrice * 0.9
}
// 处理大促价
if(tag === 'onSale') {
if(originPrice >= 100) {
return originPrice - 30
}
return originPrice * 0.8
}
// 处理返场价
if(tag === 'back') {
if(originPrice >= 200) {
return originPrice - 50
}
return originPrice
}
// 处理尝鲜价
if(tag === 'fresh') {
return originPrice * 0.5
}
}
改造后
// 定义一个询价处理器对象
const priceProcessor = {
pre(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
},
onSale(originPrice) {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
},
back(originPrice) {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
},
fresh(originPrice) {
return originPrice * 0.5;
},
};
// 询价函数
function askPrice(tag, originPrice) {
return priceProcessor[tag](originPrice)
}
priceProcessor.newUser = function (originPrice) {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
}
\
状态模式
把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行
class CoffeeMaker{
constructor(){
// 这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
// 初始化状态,没有切换任何咖啡模式
this.state="init";
// 初始化牛奶的存储量
this.leftMilk='500ml'
}
stateToProcessor={
that:this,
american(){
// 尝试在行为函数里拿到咖啡机实例的信息并输出
console.log('咖啡机现在的牛奶存储量是:',this.that.leftMild);
console.log('我只吐黑咖啡');
},
latte(){
this.american()
console.log('加点奶')
},
vanillaLatte(){
this.latte()a
console.log('再加香草糖浆')
},
mocha(){
this.latte()
console.log('再加巧克力')
}
}
// 关注咖啡机状态切换函数
changeState(state){
this.state=state
if(!this.statsToProcessor[state]){
return
}
this.stateToProcessor[state]()
}
}
const mk=new CoffeeMaker()
mk.changeState('latte')
\
\
\
策略模式与状态模式的区别
- 策略模式和状态模式事相似的,它们都封装行为、都通过委托来实现行为分发
- 策略模式中的行为函数是“潇洒”的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水
- 状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;正因为关联着同样的一个主体,所以不同状态对应的行为函数可能并不会特别割裂
观察者模式——发布/订阅模式
Vue的响应式原理
在Vue数据双向绑定的实现逻辑里,有这样三个关键角色:
- observer(监听器):注意,此observer非彼observer,所谓的observer不仅是一个数据监听器,它还需要对监听到的数据进行转发——也就是说它同时还是一个发布者
- watcher(订阅者):observer把数据转发给真正的订阅者——watcher对象。watcher接收到新的数据后,会去更新视图
- compile(编译器):MVVM框架特有的角色,负责对每个节点元素指令进行扫描和解析,指令的数据初始化、订阅者的创建这些“杂活”也归它管
\
实现observer
首先我们需要实现一个方法,这个方法会对需要监听的数据对象进行遍历、给它的属性加上定制的getter和setter函数,这样单反这个对象的某个属性发生了改变,就会触发setter函数,进而通知到订阅者,这个setter函数,就是我们的监听器:
// observe方法遍历并包装对象属性
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){
// 属性值也可能是object类型,这种情况下需要调用observe进行递归便利
observe(val)
// 为当前属性安装监听器
Object.defineProperty(target,key,{
// 可枚举
enumerable:true,
// 不可配置
configurable:false,
get:function(){
return val;
},
// 监听器函数
set:function(value){
// 通知所有订阅者
dep.notify()
}
})
}
下面实现订阅者Dep:
// 定义订阅者Dep
class Dep{
constructor(){
// 初始化订阅队列
this.subs=[]
}
// 增加订阅者
addSub(sub){
this.subs.push(sub)
}
// 通知订阅者(是不是所有的代码都似曾相识)
notify(){
this.subs.forEach((sub)=>{
sub.update()
})
}
}
\
实现一个Event Bus/Event Emitter
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[evnetName].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)
}
}
观察者模式和发布-订阅模式的区别
- 发布者直接触及到订阅者的操作,叫做观察者模式
- 发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信和操作,叫做发布-订阅模式
\
为什么要有观察者模式?观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用,两者之间还是有着说不清、道不明的关系。
而发布-订阅模式,则是快刀斩乱麻了——发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。
但这并不意味着,发布-订阅模式就比观察者模式“高级”。在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么我们使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。
\
迭代器模式
写一个能够生成迭代器对象的迭代器生成函数
// 定义生成器函数,入参是任意集合
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()