前端JavaScript中常见设计模式
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
本文主要来学习前端常用的设计模式。设计模式是软件设计中的一种思维方式,并不是某种语言所独有的,java、C 等各种语言都可以遵循这些设计模式来进行代码的编写。
作为一名前端开发人员,可以使用 JavaScript 语言来将这些设计模式呈现出来,在工作中也能够应用上。
可以进一步将设计模式拆分为 设计原则 和 设计模式:
设计原则
设计原则:软件设计要遵循的原则。
- 单一职责原则:每个类或方法只做一件事
- 开闭原则:对外开放,对内关闭,尽量少的去更改底层的功能,将方法暴露给外部去修改
- 里氏替换原则:子类继承父类,尽量不要去修改覆盖父类的方法
- 迪米特法则:不推荐跨层级通信(如vue中爷孙组件通信时,可以使用Vuex传递数据,不建议直接跨层级通信)
- 接口隔离原则:接口设计最小单元,高内聚、低耦合
- 依赖倒置原则:抽象出来的方法、类应该是共性的内容,而不是很细节的内容
设计模式
设计模式 是软件开发人员在开发过程中面临一些具有代表性问题的解决方案,也可以说是我们的前辈在踩了一些坑之后,为我们总结出来的一些解决问题的通用方案。
遵循设计模式编写出来的代码 复用性 和 可扩展性 会更加明显,利于维护。
设计模式 将 设计规则具体化,通常分为三大类:
- 创建型
- 结构性
- 行为型
常见的 设计模式 有以下几种:
- 单例模式
- 工厂模式
- 装饰器模式
- 观察者模式
- 代理模式
- 适配器模式
- .......
这些模式都属于上面提到的三大类中的某一类型。下面来具体学习一下这些设计模式。
单例模式
单例模式(Singleton Pattern) 属于创建型的一种常用的软件设计模式,是指全局同时只能有一个实例,也要保证一个类只能有一个实例,并要提供全局可调用的接口。这样就算实例化多次,也不会创建多个实例,可以节约性能。我们常见的 window
document
以及 vue 中的 Store
都是单例模式。
实现一个简单的单例模式:
// 利用静态属性值 instance 实现单例模式
class Person {
static instance
constructor(name) {
if(!Person.instance) {
Person.instance = this
} else {
return Person.instance
}
this.name = name
}
}
let ci = new Person('cilly')
let to = new Person('tom')
console.log(ci, to)
console.log(ci===to) // true
如果有多个类都要实现单例模式的话,需要在每个类中都定义上面这样的静态属性,会显得很麻烦。这时候可以编写一个通用方法来实现:
// 比如要将下面动物类和水果类变为单例模式
class Animal{
constructor(name) {
this.name = name
}
}
class Fruit{
constructor(name) {
this.name = name
}
}
// 通用创建单例模式的方法具体实现
function createSingle(Fn) {
let instance // 通过闭包缓存 instance
return function(...args) {
if(!instance) {
instance = new Fn(...args)
}
return instance
}
}
// 使用
const singleAnimal = createSingle(Animal)
const dog = new singleAnimal('dog')
const pig = new singleAnimal('pig')
console.log(dog, pig)
console.log(dog === pig) // true
利用闭包来缓存 instance
,就不用在每个类里面都定义一个静态属性。
单例模式的应用场景:
下面创建一个弹窗,要求控制每个实例只能有一个弹窗,并且全局范围内只能有一个实例。利用单例模式来解决:
// 弹窗
class Dialog {
constructor() {
this.dialog = document.createElement('div')
this.dialog.innerHTML = '弹窗'
this.dialog.style.display = 'none'
this.isShow = false
}
show() {
if(!this.isShow) {
document.body.append(this.dialog)
this.dialog.style.display = 'block'
this.isShow = true
} else {
console.log('当前存在弹窗')
}
}
}
// 使用
const dialog = new Dialog()
document.querySelector('button').onclick = function() {
dialog.show()
}
这样 实现了一个类只创建一次弹窗,但是如果实例化两次就能创建两个弹窗,如下:
// 使用
const dialog = new Dialog()
const dialog2 = new Dialog()
document.querySelector('button').onclick = function() {
dialog.show()
dialog2.show()
}
解决思路是:通过 createSingle
方法来限制全局范围内只能有一个实例,这样就实现了每个实例只能有一个弹窗,并且全局范围内只能有一个实例。如下:
// 使用
// const dialog = new Dialog()
// const dialog2 = new Dialog()
const createDialog = createSingle(Dialog)
const dialog = new createDialog()
const dialog2 = new createDialog()
document.querySelector('button').onclick = function() {
dialog.show()
dialog2.show()
}
单例模式优缺点
- 优点:节约内存开支和实例化时的性能开支
- 缺点:扩展性不强
工厂模式
工厂模式(Factory Pattern):封装具体实例创建过程和逻辑,外部可以根据不同的条件创建不同的实例。
工厂模式的简单示例:
class Dog {
constructor() {
this.name = '旺旺'
}
}
class Cat {
constructor() {
this.name = '喵喵'
}
}
// 工厂模式
function Factory(key) {
switch (key) {
case 'Dog':
return new Dog()
break
case 'Cat':
return new Cat()
break
default:
break
}
}
// 使用
const ww = Factory('Dog')
const mm = Factory('Cat')
console.log(ww, mm) // Dog {name: '旺旺'} Cat {name: '喵喵'}
console.log(ww.name, mm.name) // 旺旺 喵喵
工厂模式优缺点
- 优点:功能封装后代码具有复用性,抽象逻辑
- 缺点:增加代码复杂度
装饰器模式
装饰器(或装饰者)模式(Decorator Pattern):允许向一个现有的对象添加新的功能,同时又不改变其结构。
简单示例1:
class Person {
constructor() {
this.name = '张三'
}
song() {
console.log(' 唱')
}
}
// 装饰器
Person.prototype.Decorator = function(fn) {
this.song()
fn.apply(this)
}
const zs = new Person()
// zs.song()
function jump() {
console.log(this.name + ' 跳');
}
zs.Decorator(jump)
示例2:通过给 Function
添加装饰方法,形成装饰者链
// 装饰者链
Function.prototype.Decorator = function(fn) {
// 返回一个函数以便链式调用
return () => {
this()
fn()
}
}
function rap() {
console.log('rap')
}
function talk() {
console.log('talk')
}
// 链式调用
zs.song.Decorator(rap).Decorator(talk)()
示例3:对 window.onload
进行装饰
// 对 window.onload 进行装饰
window.onload = function() {
console.log('----onload');
}
const _onload = window.onload
window.onload = function() {
_onload()
console.log('这里做额外处理de一些事情...');
}
观察者模式
观察者模式(Observer Pattern):对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
addEventListener
的事件监听机制,底层就是一个观察者模式。
<button>点击</button>
document.querySelector('button').addEventListener('click', function(){
console.log('click1');
})
document.querySelector('button').addEventListener('click', function(){
console.log('click2');
})
下面用观察者模式模仿系统的addEventListener
:
class MyEvent {
constructor() {
this.handles = {} // 数据格式:{ en1: [fn1, fn2 ...], en2: [fn1, fn2 ...] }
}
addEvent(eventName, fn) {
if(typeof this.handles[eventName] === undefined) {
// handles 中不包含eventName时
this.handles[eventName] = []
}
this.handles[eventName].push(fn)
}
trigger(eventName) {
if(!(eventName in this.handles)) {
return
}
this.handles[eventName].forEach(fn => {
fn()
})
}
}
使用这个类:
const obj = new MyEvent()
obj.addEvent('event1', function(){
console.log('event1')
})
obj.addEvent('event1', function(){
console.log('event1---')
})
// 模拟obj变化时,触发监听的函数
setTimeout(function(){
obj.trigger('event1') // event1 event1---
},2000)
代理模式
代理模式(Proxy Pattern):为其他对象提供一种代理,控制对这个对象的访问。
示例:
实现一个创建图片的类,并向网页中插入一张图片
class CreateImage {
constructor() {
this.img = document.createElement('img')
document.querySelector('body').append(this.img)
}
setSrc(src) {
this.img.src = src
}
}
let img = new CreateImage()
img.setSrc('https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/haidi.ffe5bcf.png')
网络较慢或图片较大时,图片可能加载的很慢。这时可以添加一个loading动画,优化用户体验,这里就可以用到代理模式
function proxyImg(src) {
let img = new CreateImage()
let loadingImg = new Image()
img.setSrc('./img/loading.gif') // 先显示loading
loadingImg.src = src // 准备加载真实图片
// 图片加载完成
loadingImg.onload = function() {
img.setSrc(src) // 把loading替换掉
}
}
proxyImg('https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/haidi.ffe5bcf.png')
注意:代理模式要对代理对象做一些控制,否则什么都不做就没有意义了
适配器模式
适配器模式(Adapter Pattern):是作为两个不兼容的接口之间的桥梁。不兼容的接口或数据等,通过适配器的转换,可以得到期待的结果
示例:
function getList() {
return [
{
name: 'cilly',
age: 10
},
{
name: 'tom',
age: 20
}
]
}
// 适配器
function Adapter(arr) {
let res = []
for(let i = 0; i < arr.length; i++) {
const obj = {
[arr[i].name] : arr[i].age
}
res.push(obj)
}
return res
}
let res = Adapter(getList())
console.log(res) // [ {cilly: 10}, {tom: 20} ]
适配器模式优缺点
-
优点:1. 使用适配器解决兼容问题,不用修改原有逻辑,让原有逻辑可以更好的服用。2. 灵活性好,适配器不会对原有的功能造成影响,不想使用适配器时直接删掉即可。
-
缺点:过多使用适配器,会使系统变的零乱,代码复杂度增加
未完待续 ......
转载自:https://juejin.cn/post/7148398800481026085