一、概述
首先,什么是设计模式?我们可以简单理解为一些从大量的经验中总结出来的套路,使用这些套路可以帮我们有效解决问题,使代码变得更加优雅。本文将从前端的角度,系统介绍各设计模式,并通过一些例子以便大家理解。设计模式可能在平时业务中很少用到,但对于比较复杂的项目,或者想要自己开发一些工具或组件的时候,还是非常实用的,你可以在平时开发中根据自己的业务择机使用。就算用不到,学习设计模式也可以让你具备以更高层次看待代码的能力。本文是学习《kerwin的设计模式》后,总结而写,各位看官请耐心看完或者先收藏,慢慢阅读。大家有兴趣可以在b站一键三连,支持优秀的老师,支持免费的网课,然后免费学习视频。
其次,本文需要良好的JS基础,如需要熟悉原型、类、迭代器、链表结构等。希望你在阅读本文前已经熟悉这些知识。如果没有这些前置知识,你依然可以阅读这篇文章,当遇到难以理解的知识点时可以单独对该知识点内容进行学习,或者暂时跳过。
二、走进设计模式
2.1 工厂模式
工厂模式,顾名思义,可以批量生产对象,对于一些拥有共同属性或方法的对象,我们可以封装一个工厂,工厂中包含通有的属性和方法,通过传入不同参数,来制造不同的对象。
// 举例:后台管理系统,登录不同账号,展示不同页面
// 工厂类,构造函数中包含角色和页面,类的静态方法中包含根据角色返回页面的工厂函数
class User {
constructor(role, pages) {
this.role = role
this.pages = pages
}
static UserFactory(role) {
switch (role) {
case 'superadmin':
return new User('superadmin', ['home', 'user-manage', 'right-manage', 'news-manage'])
case 'admin':
return new User('admin', ['home', 'user-manage', 'news-manage'])
case 'editor':
return new User('editor', ['home', 'news-manage'])
default:
throw new Error('参数错误')
}
}
}
// 直接调用静态方法即可即可生成目标对象
const superadmin = User.UserFactory('superadmin')
const admin = User.UserFactory('admin')
2.2 抽象工厂模式
抽象工厂模式基于工厂模式改造,不直接生产对象,而是生产类。先有一个父类,包含公有的属性和方法,但是公有的方法并没有函数体,函数体需要在子类中实现。有多个子类继承父类,并拥有自己独特的属性和方法
// 以2.1的例子继续改造
// 父类
class User {
constructor(name, role, pages) {
this.name = name
this.role = role
this.pages = pages
}
welcome() {
console.log('欢迎回来', this.name)
}
dataShow() {
// 抽象类,需在子类实现,js暂无此关键字,可参考ts用法
throw new Error('js中抽象方法需要在子类实现')
}
}
// 每一个子类都继承自父类并且可以扩展自己的属性和方法
class SuperAdmin extends User {
constructor(name) {
super(name, 'superadmin', ['home', 'user-manage', 'right-manage', 'news-manage'])
}
dataShow() {
console.log('super-admin---datashow')
}
addRight() {}
addUser() {}
}
class Admin extends User {
constructor(name) {
super(name, 'admin', ['home', 'user-manage', 'news-manage'])
}
dataShow() {
console.log('admin---datashow')
}
addUser() {}
}
class Editor extends User {
constructor(name) {
super(name, 'editor', ['home', 'news-manage'])
}
dataShow() {
console.log('editor---datashow')
}
}
// 这是抽象工厂,只返回类
function getUserClassFactory(role) {
switch (role) {
case 'superadmin':
return SuperAdmin
case 'admin':
return Admin
case 'editor':
return Editor
default:
throw new Error('参数错误')
}
}
// 根据需求拿到所需类后再实例化使用
const currentClass = getUserClassFactory('superadmin')
const currentRole = new currentClass('zjm')
currentRole.dataShow()
2.3 建造者模式
建造者模式关注过程和细节,将复杂对象的构建层和表示层相互分离,如果两个类具有同样的执行逻辑,那么此时可以新建一个类,在该类中统一执行逻辑
// 举例:此时有Navbar和List两个类,两个类都有init,getData,render方法,那么我们就可以创建一个新类来执行逻辑,而Navbar和List两个类只需关心自己内部的使用逻辑。
class Navabar {
init() {
console.log('navabar-init')
}
getData() {
console.log('navabar-getdata')
return new Promise((resolve) => {
setTimeout(() => {
resolve('navabar-111')
}, 1000)
})
}
render() {
console.log('navabar-render')
}
}
class List {
init() {
console.log('list-init')
}
getData() {
console.log('list-getdata')
return new Promise((resolve) => {
setTimeout(() => {
resolve('list-111')
}, 1000)
})
}
render() {
console.log('list-render')
}
}
//内部创建一个函数,用于统一执行逻辑
class Creater{
async startBuild(builder) {
builder.init()
await builder.getData()
builder.render()
}
}
const operator = new Creater()
operator.startBuild(new Navabar())
operator.startBuild(new List())
2.4 单例模式
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点,主要解决一个全局使用的类频繁地销毁和创建,占用内存
// 以登录过期时,跳出对话框为例,该对话框在过期时才创建,期间对话框并不销毁,此处ES5实现还利用了闭包
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.modal {
position: fixed;
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
background-color: pink;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
<body>
<button class="open">打开</button>
<button class="close">关闭</button>
<script>
// ES5实现,通过闭包保存了instance,以保证每次调用返回的都是最初创建的那个实例
// const Single = (function() {
// let instance = null
// return function() {
// if (!instance) {
// instance = document.createElement('div')
// instance.innerHTML = '登录对话框'
// instance.className = 'modal'
// document.body.appendChild(instance)
// }
// return instance
// }
// })()
// ES6实现
class Single {
// 注意,这里要使用静态属性和方法,这样instance和方法是属于类的
// 如果不加Single,那instance是属于实例的,每次就会创建多个实例
static instance = null
static getInstance() {
if(!Single.instance) {
Single.instance = document.createElement('div')
Single.instance.innerHTML = '登录对话框'
Single.instance.className = 'modal'
document.body.appendChild(Single.instance)
}
return Single.instance
}
}
document.querySelector('.open').onclick = function() {
const modal = Single.getInstance()
modal.style.display = 'block'
}
document.querySelector('.close').onclick = function() {
const modal = Single.getInstance()
modal.style.display = 'none'
}
</script>
</body>
</html>
2.5 装饰器模式
装饰器模式可以很好的对已有功能进行扩展,不会更改原有代码
/*
1.这里举一个应用场景,点击某个选项时,有时具有附加功能,需要先给后台发送统计数据,以对展示内容做出调整;之后才正式执行业务逻辑;
2.因此这里实现一个前置函数,在执行业务逻辑前执行
*/
Function.prototype.before = function(beforeFn) {
const _this = this
return function() {
// 执行前置函数
beforeFn.apply(this, arguments)
// 执行原函数并返回原返回值
return _this.apply(this, arguments)
}
}
// 向后台发送统计数据(前置函数)
function log() {
console.log('发送pv等统计数据')
}
// 原有的业务逻辑
function render() {
console.log('执行业务逻辑,如跳转页面等')
}
// 一般log函数是异步的,因此在执行完log函数后在执行render函数,以往我们可能会将render函数的调用放在log函数内部,这样会导致高耦合,且违反了函数的单一功能原则,不推荐
// 如有需要时可以重写render函数,以执行前置函数
render = render.before(log)
// 利用定时器模拟按钮点击时,执行的操作
setTimeout(() => {
render()
}, 1000)
2.6 适配器模式
将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作
// 举例,点击某一个按钮需要渲染百度地图,点击另一个按钮需要渲染高德地图
class GDMap {
show() {
console.log('渲染高德地图--粘贴的一堆代码')
}
}
class BDMap {
dispaly() {
console.log('渲染百度地图--粘贴的一堆代码')
}
}
// 创建适配器,以使不同的地图都可以调用同一个方法
class BDMapAdapter extends BDMap{
constructor() {
super()
}
show() {
this.dispaly()
}
}
// 该方法不管是要渲染百度地图或是高德地图都可以使用
function renderMap(map) {
map.show()
}
renderMap(new GDMap())
renderMap(new BDMapAdapter())
2.7 策略模式
策略模式将算法(这里算法可以是一些简单的逻辑)的实现和使用分离开来,避免大量使用if和else进行判断,提高代码可读性。
// 这里以一个列表为例,列表每一项左侧是新闻名字,右侧是新闻状态
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
li {
display: flex;
justify-content: space-around;
}
.green-text {
color: green;
}
.yellow-text {
color: yellow;
}
.red-text {
color: red;
}
</style>
</head>
<body>
<ul class="cont"></ul>
<script>
const list = [
{ title: '新闻1', type: 1 },
{ title: '新闻2', type: 1 },
{ title: '新闻3', type: 2 },
{ title: '新闻4', type: 3 },
]
// 直接将算法(这里仅是一些简单的属性)提炼出来
const listMapStrategy = {
1: {
text: '审核中',
className: 'green-text'
},
2: {
text: '待审核',
className: 'yellow-text'
},
3: {
text: '已驳回',
className: 'red-text'
}
}
const cont = document.querySelector('.cont')
cont.innerHTML = list.map(item => {
return `
<li>
<span>${item.title}</span>
<span class='${listMapStrategy[item.type].className}'>${listMapStrategy[item.type].text}</span>
</li>
`
}).join('')
</script>
</body>
</html>
2.8 代理模式
代理模式为其他对象提供一种代理,以控制对这个对象的访问,相当于原对象把控制权交出去了
// ES6的proxy专为代理而生,可以执行非常多的操作,如对proxy不了解的,可以去单独学习
// 简单来说,通过new Proxy创建一个实例,当访问这个代理对象时,会调用其中的get方法;修改代理对象的值时,会调用其中的set方法
let obj = {
name: 'zjm',
age: 18
}
let proxyObj = new Proxy(obj, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
}
})
proxyObj.age = 17
console.log(proxyObj.name)
2.9 观察者模式
观察者模式包含观察目标和观察者,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。观察者模式的缺点在于无法对事件的细分管控,只有一个notify方法,无法控制如'aaa'事件触发还是'bbb'事件触发。
注意:这里针对观察者模式和下一个模式-发布订阅者模式,不做过多讲解,感兴趣的朋友可以移步我之前的文章《js手写一:实现发布订阅者模式》。
// 模拟点击菜单,右边和头部展示面包屑的功能(注意,实际开发时,左右组件是互不关联的)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
display: flex;
height: 800px;
}
.box .left {
width: 500px;
height: 100%;
background-color:pink;
}
.box .left li {
cursor: pointer;
}
.box .right {
flex: 1;
background-color: yellow;
}
</style>
</head>
<body>
<header class="header">路径</header>
<div class="box">
<div class="left">
<ul>
<li>首页</li>
<li>用户管理</li>
<li>新闻管理</li>
<li>权限管理</li>
</ul>
</div>
<div class="right">
<div class="bread">ddd</div>
</div>
</div>
<script>
// 目标
class SubJect {
constructor() {
this.observers = []
}
// 添加观察者
add(observer) {
this.observers.push(observer)
}
// 移除观察者
remove(observer) {
this.observers = this.observers.filter(item => item !== observer)
}
// 通知观察者执行
notify(data) {
this.observers.forEach(item => item.update(data))
}
}
// 观察者
class Observer {
constructor(name) {
this.ele = document.querySelector(name)
}
// 观察者数据更新
update(data) {
this.ele.innerHTML = data
}
}
// 创建目标和观察者并添加观察者
const subject = new SubJect()
const observer1 = new Observer('.bread')
const observer2 = new Observer('.header')
subject.add(observer1)
subject.add(observer2)
// 测试代码:点击列表时,面包屑和头部数据更新为点击的数据项
const lis = document.querySelectorAll('li')
lis.forEach(li => {
li.addEventListener('click', () => {
subject.notify(li.innerHTML)
})
})
</script>
</body>
</html>
2.10 发布订阅者模式
需要通信时,首先需要主动订阅,和观察者模式的区别在于增加了订阅的事件类型。发布者和订阅者互不相关,对于他们来说,只需关注自己发出/接收数据就行,其他不用关心。
这个模式大家应该都很熟悉,Vue中的事件总线就是使用的发布订阅者模式,大家可以参照平时对事件总线的使用来更好的理解这个设计模式。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
display: flex;
height: 800px;
}
.box .left {
width: 500px;
height: 100%;
background-color:pink;
}
.box .left li {
cursor: pointer;
}
.box .right {
flex: 1;
background-color: yellow;
}
</style>
</head>
<body>
<header class="header">路径</header>
<div class="box">
<div class="left">
<ul>
<li>首页</li>
<li>用户管理</li>
<li>新闻管理</li>
<li>权限管理</li>
</ul>
</div>
<div class="right">
<div class="bread">ddd</div>
</div>
</div>
<script>
// 同2.9观察者模式的例子
class PubSub {
constructor() {
this.message = {}
}
// 增加了订阅的细分类型
subscribe(type, fn) {
if (!this.message[type]) {
this.message[type] = []
}
this.message[type].push(fn)
}
// 发布事件
publish(type, params) {
if(!this.message[type]) return
this.message[type].forEach(item => item(params))
}
// 解除订阅
unsubscribe(type = null) {
if (!type) {
this.message = {}
} else {
this.message[type] = []
}
}
}
const pubsub = new PubSub()
// 使用时,需先订阅,再发布,订阅者接到发布的通知时,则会依次调用
pubsub.subscribe('bread',(data) => {
const targetNode = document.querySelector('.bread')
targetNode.innerHTML = data
})
pubsub.subscribe('header',(data) => {
const targetNode = document.querySelector('.header')
targetNode.innerHTML = data + '--header'
})
const lis = document.querySelectorAll('li')
lis.forEach(li => {
li.addEventListener('click', () => {
pubsub.publish('bread', li.innerHTML)
pubsub.publish('header', li.innerHTML)
})
})
setTimeout(() => {
// pubsub.unsubscribe('header')
pubsub.unsubscribe()
}, 3000);
</script>
</body>
</html>
2.11 模块模式
模块模式对前端来说就是模块化开发,ES6Module通过 export 导出模块, 通过import导入模块,各模块根据导入导出关系建立联系。目前还在使用的还有CommonJs模块,这些是比较基础的前端知识,这里仅一带而过。
2.12 桥接模式
将抽象部分与他的实现部分分离,使他们都可以独立地变化,使用场景:一个类存在两个或多个独立变化的维度,且这两个维度都需要进行扩展
优点:有助于独立管理各组成部分
缺点:每使用一个桥接元素都要增加一次函数调用,有可能会提高系统的复杂程度。
这里以弹窗的动画效果和弹窗的形式举例(toast, dialog这些)。UI框架中,有很多诸如弹窗或者提示框这些类型,也有很多弹出的动画效果。弹窗效果可以内部扩展,弹窗形式也可以根据需求定义多个类,看完代码就很好理解了。
// 动画类(这里仅用个对象表示了),内部可以扩展多个动画,且实现了具体方法
const Animations = {
debounce: {
show(ele) {
console.log(ele, '弹跳显示')
},
hide(ele) {
console.log(ele, '弹跳隐藏')
}
},
slide: {
show(ele) {
console.log(ele, '滑动显示')
},
hide(ele) {
console.log(ele, '滑动隐藏')
}
},
rotate: {
show(ele) {
console.log(ele, '旋转显示')
},
hide(ele) {
console.log(ele, '旋转隐藏')
}
}
}
// 对话框,原型上有show方法,但是没有作具体实现,调用传入元素的show方法
function Toast(ele, animation) {
this.ele = ele
this.animation = animation
}
Toast.prototype.show = function() {
// 抽象,即不具体实现show方法
this.animation.show(this.ele)
}
Toast.prototype.hide = function() {
this.animation.hide(this.ele)
}
// 模态框
function Modal(ele, animation) {
this.ele = ele
this.animation = animation
}
Modal.prototype.show = function() {
// 抽象,即不具体实现show方法
this.animation.show(this.ele)
}
Modal.prototype.hide = function() {
this.animation.hide(this.ele)
}
// 弹窗类型和弹窗动画可以自由组合
const toast1 = new Toast('div1', Animations.debounce)
const rotate1 = new Modal('div2', Animations.rotate)
toast1.show()
rotate1.show()
setTimeout(() => {
rotate1.hide()
}, 1000)
2.13 组合模式
组合模式在对象间形成树形结构,无需关心对象有多少层,调用时只需在根部进行调用,因此会使用到迭代。下面以两个例子来帮助理解
2.13.1 扫描文件夹
/*
遍历文件夹,文件夹下存在文件,结构如下
root
html
html1
html2
css
css
js
js
*/
// 这里用js模拟,为简单起见不使用node模块,因此定义add方法手动添加
// 文件夹类
class Folder {
constructor(folder) {
this.folder = folder
// 收集子文件夹
this.list = []
}
add(folder) {
this.list.push(folder)
}
scan() {
console.log('开始扫描文件夹', this.folder)
for(const item of this.list) {
// 递归调用子目录下的scan方法,因此文件类中必须也实现scan方法
item.scan()
}
}
}
// 文件类
class File {
constructor(file) {
this.file = file
this.list = []
}
scan() {
console.log('开始扫描文件', this.file)
}
}
// 添加文件
const rootFolder = new Folder('root')
const htmlFolder = new Folder('html')
const jsFolder = new Folder('js')
const cssFolder = new Folder('css')
rootFolder.add(htmlFolder)
rootFolder.add(jsFolder)
rootFolder.add(cssFolder)
const htmlFile1 = new File('html1')
const htmlFile2 = new File('html2')
const cssFile = new File('cssFile')
const jsFile = new File('jsFile')
htmlFolder.add(htmlFile1)
htmlFolder.add(htmlFile2)
cssFolder.add(cssFile)
jsFolder.add(jsFile)
// 只需调用根节点的方法,就会依据树结构递归调用其余节点
rootFolder.scan()
// 开始扫描文件夹 root
// 开始扫描文件夹 html
// 开始扫描文件 html1
// 开始扫描文件 html2
// 开始扫描文件夹 js
// 开始扫描文件 jsFile
// 开始扫描文件夹 css
// 开始扫描文件 cssFile
2.13.2 动态渲染菜单侧边栏
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">
// 目标是实现这种结构的侧边栏,其实结构就是2.13.1注释中的文件夹结构,因此,根据2.13.1改造就行,将原本的打印变为一些dom操作
<!-- <ul>
<li>用户管理
<ul>
<li>新建用户</li>
<li>编辑用户</li>
</ul>
</li>
<li>权限管理</li>
<li>新闻管理</li>
</ul> -->
</div>
<script>
class Folder {
constructor(folder) {
this.folder = folder
this.list = []
}
add(folder) {
this.list.push(folder)
}
scan() {
let childUl = null
if (this.folder === 'root') {
console.log('不创建节点')
} else {
const ulEle = document.createElement('ul')
const liEle = document.createElement('li')
liEle.innerHTML = this.folder
// 这里有个相对难的就是第一次扫描时,需要在每一个li下创建一个ul并传递给下一级,以作为下一级目录的父节点
childUl = document.createElement('ul')
liEle.appendChild(childUl)
ulEle.appendChild(liEle)
root.appendChild(ulEle)
}
for(const item of this.list) {
item.scan(childUl)
}
}
}
class File {
constructor(file) {
this.file = file
this.list = []
}
scan(ele) {
const liEle = document.createElement('li')
liEle.innerHTML = this.file
ele.appendChild(liEle)
}
}
const rootFolder = new Folder('root')
const htmlFolder = new Folder('用户管理')
const jsFolder = new Folder('权限管理')
const cssFolder = new Folder('新闻管理')
rootFolder.add(htmlFolder)
rootFolder.add(jsFolder)
rootFolder.add(cssFolder)
const htmlFile1 = new File('新增用户')
const htmlFile2 = new File('查询用户')
const cssFile = new File('新增权限')
const jsFile = new File('新增新闻')
htmlFolder.add(htmlFile1)
htmlFolder.add(htmlFile2)
cssFolder.add(cssFile)
jsFolder.add(jsFile)
rootFolder.scan()
</script>
</body>
</html>
2.14 命令者模式
命令者模式由三种角色构成:发布者(invoker),接收者(receiver),命令对象(command)。发布者和接收者通过命令对象建立连接,彼此之间不需要知道对方的具体信息。这个模式的实现思路有点意思,可以为我们其他类似问题的处理提供思路。
Q:实现思路,如何建立联系?
A:通过参数传递,如发布者构造函数接收一个command,而command中实现了一个execute方法,而receiver中也实现了一个execute方法。因此,一调用发布者的execute方法,就会执行comand中的execute,已调用command中的execute方法,就会调用receiver的execute方法
class Invoker {
constructor(command) {
this.command = command
}
execute() {
console.log('发布者执行了')
this.command.execute()
}
}
class Command {
constructor(receiver) {
this.receiver = receiver
}
execute() {
console.log('命令对象-接收者如何执行')
this.receiver.execute()
}
}
class Receiver {
execute() {
console.log('接收者执行了')
}
}
const receiver = new Receiver()
const command = new Command(receiver)
const invoker = new Invoker(command)
// 发布者执行,间接通过命令对象调用执行者
invoker.execute()
2.15 宏命令模式
命令模式业务场景可能比较少用,命令模式和组合模式结合就形成了组合命令模式,即宏命令模式。
举例:页面加载后,页面中的轮播图,瀑布流,选项卡等一起开始调用。
class MircoCommand {
constructor() {
// 初始化list用于保存所有command
this.list = []
}
add(command) {
this.list.push(command)
}
execute() {
// 遍历list,调用每一个command中的execute,因此需要保证每一个command中有一个execute方法
for(const item of this.list) {
item.execute()
}
}
}
const Tabs = {
execute() {
console.log('tabs开始调用')
// 可以单独对每一个功能模块进行扩展
this.init()
this.getData()
},
init() {
console.log('tabs-init')
},
getData() {
console.log('tabs-getData')
}
}
const Swipper = {
execute() {
console.log('swipper开始调用')
},
}
const mircoCommand = new MircoCommand()
mircoCommand.add(Tabs)
mircoCommand.add(Swipper)
// 唯一的执行者,发布命令后,每个子系统开始执行
mircoCommand.execute()
2.16 迭代器模式
迭代器模式不关心对象的内部构造,也可以按顺序访问其中的每个元素。迭代器是ES6的一个语法,他不是什么神秘的东西,这里不对其进行详细描述,可以理解为迭代器就是在内部实现了一个[Symbol.iterator]方法,而这个方法一定实现了一个next函数,通过调用next函数来逐步执行。部署了迭代器的对象可以通过for of遍历,如数组,字符串,arguments等,这里举三个例子来帮助理解迭代器模式
1.一个普通的数组,不通过for of遍历,通过next逐步调用,查看结果
2.一个类似于数组的对象,手动部署一个迭代器以使其可以被for of遍历
3.一个普通对象,使其可以被for of遍历
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//
const list = ['111', '222', '333']
// 1.在浏览器打印可看到该数组的原型身上具有一个Symbol.iterator方法
console.log(list, list[Symbol.iterator]())
// 因此,可迭代对象除了直接遍历外,仍然可以通过这种方法遍历调用(for of 本质就是根据lenth属性调用next方法)
let iterator = list[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
// 2.给类似于数组的对象部署一个迭代器,那么我们也可以通过for of遍历
let obj = {
0: 'zjm',
1: '18',
length: 2,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for(const item of obj) {
console.log(item)
}
// 3.给普通的对象部署一个迭代器
// 目的是让这个对象可以进行for of遍历,并且拿到的是其中某一个数组的每一项
const obj2 = {
code: 200,
msg: 'success',
data: ['111', '222', '333'],
[Symbol.iterator]: function() {
let index = 0
return {
next:() => {
if (index < this.data.length) {
return {
value: this.data[index++],
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
}
}
// for (const item of obj2) {
// console.log(item)
// }
const it = obj2[Symbol.iterator]()
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
</script>
</body>
</html>
2.17 职责链模式
职责链模式使多个对象都有机会处理请求,从而避免了请求的发送者与多个接收者直接的耦合关系。将这些接收者连接成一条链,顺着这条链传递该请求,直到找到能处理该请求的对象。
优点:符合单一职责,易于扩展。
这里以表单校验为例来说明职责链模式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input id="input" type="text">
<button id="btn">注册</button>
<script>
// 普通写法,需要加入大量判断,耦合性强,
// btn.addEventListener('click', () => {
// const value = input.value
// if (!value) {
// console.log('请输入内容')
// } else if (isNaN(value)) {
// console.log('请输入数字')
// } else if (value.length < 6) {
// console.log('长度需大于5位')
// } else {
// console.log('通过校验')
// }
// })
// 职责链模式
function checkEmpty() {
if (!input.value) {
console.log('请输入内容')
return
}
return 'next'
}
function checkIsNumber() {
if (isNaN(input.value)) {
console.log('请输入数字')
return
}
return 'next'
}
function checkLength(value) {
if (input.value.length < 6) {
console.log('长度需大于5位')
return
}
return 'next'
}
/*
这里可能有个地方大家觉得比较难理解,如第二次调用addRule方法,用代码推演下就容易理解了
constructor(nextRule) {
this.checkRule = nextRule
this.nextRule = null
}
nextRule传入后经过实例化,得到的this.nextRule是一个实例对象,和第一次的nextRule是没关系的,即链表中节点的传递
*/
class CheckChain {
constructor(checkRule) {
this.checkRule = checkRule
this.nextRule = null
}
check() {
this.checkRule() === 'next' ? this.nextRule.check() : null
}
addRule(nextRule) {
this.nextRule = new CheckChain(nextRule)
return this.nextRule
}
// 用于最后一次调用,因为每次校验都会调用check方法,这里结尾时重写check方法
end() {
this.nextRule = {
check() {
console.log('校验成功')
}
}
}
}
btn.addEventListener('click', () => {
const check = new CheckChain(checkEmpty)
// 直接链式调用即可
check.addRule(checkIsNumber).addRule(checkLength).end()
check.check()
})
</script>
</body>
</html>
三、总结
有些设计模式我们平时有所接触,但有些设计模式比较陌生。上述的设计模式代码实现还是有一定难度的,值得好好去理解一下,我也在不断学习中,各位看客如果觉得本文对你有帮助,可以点赞收藏,希望在前端路上可以和大家一起慢慢进步成长。