盘点前端开发中最常见的几种设计模式

6,056 阅读7分钟

设计模式介绍

设计模式是开发的过程中,遇到一些问题时的解决方案,这些方案是通过大量试验与踩坑总结出来的最佳实践,主要指的是一些代码思想与方法论,而不是指现场的代码。本文主要总结前端开发中时常遇到的一些设计模式。

设计模式的五大原则

  • 单一职责 一个程序只做一件事,如果功能过于复杂就拆分开,每个部分保持独立
  • 开发封闭 对扩展开放,对修改封闭 增加需求时,扩展新代码,而非修改旧代码
  • 李氏置换 子类能覆盖父类(继承)
  • 接口独立 类似于单一职责,关注于接口 保存接口的单一独立
  • 依赖倒置 只关注接口而不关注具体类的实现

简单的举个栗子🌰

function loadImg (src) {
  let p = new Promise(function (resolve, reject) {
    let img = document.createElement('img')
    img.onload = function () {
      resolve(img)
    }
    img.onerror = function () {
      reject('加载失败')
    }
    img.src = src;
  })
  return p
}
let src = 'https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/311a30cc2e1d1dc418b7aec40de4c6fb~300x300.image'
let result = loadImg(src)

result.then((img) => {
  //   part1
  console.log(img.width)
  return img
  // 增加需求,增加then
}).then((img) => {
  //   part2
  console.log(img.height)
}).then(() => {
  // ...
}).catch((err) => {
  console.log(err)
})

以上通过一个简单的Promise示范了设计模式中的单一职责原则开发封闭原则,一个then中只做一件事情;如果有多个需求,增加多个then,每个then里保持独立。

如何学习设计模式?

问题抛出

身边很多前端小伙伴都存在一个疑惑:设计模式需要学习吗?需要去死记硬背吗?怎么样才算是学会了呢?是不是在日常的开发中每写一个方法都要去使用设计模式呢?

学习方法

其实,设计模式强调的是思想多于编码,我们通过阅读各种框架的源码,会发现里面大量使用设计模式进行开发,是由于开发人员已经养成一种习惯,自然而然的就会使用到代码中,所以形成思想是至关重要的。对此,就如何学习设计模式这个问题,我提出自己的一些看法

  • 明白每个设计的道理和用意
  • 通过每个设计模式的经典示例应用体会它的真正使用场景
  • 自己编码时多思考,可以尽量模仿
  • 好记性不如烂笔头
  • 祈祷奇迹不如无尽的练习

前端常见的设计模式

工厂模式

简介

工厂模式主要是为创建实例提供了接口,将new操作进行单独封装

举个例子:

小明去华莱士买汉堡,直接点餐、取餐,不用自己亲手做

而华莱士要封装做汉堡的操作,做好直接卖给小明

示例

class Hamburger {
  constructor(name) {
    this.name = name
  }
  init () {
    console.log('汉堡初始化')
  }
}

// 华莱士相当于一个工厂,封装了返回汉堡的示例的操作
class Hls {
  create (name) {
    return new Hamburger(name)
  }
}

// 新建一个华莱士
let hls = new Hls()
// 通过create方法返回一个示例
let hamburger1 = hls.create('香辣鸡腿堡');
hamburger1.init()

场景

jQuery

在jQuery中,我们去获取一个元素,是通过操作符,而不是通过new一个个实例,其实操作符,而不是通过new一个个实例,其实方法实现的形式就是使用了工厂模式

代码演示
class jQuery {
  constructor(selector) {
    // ...
  }
  append() {}
  addClass() {}
  // ...
}

window.$ = function (selector) {
  return new jQuery(selector)
}
React中的createElement

通过返回VNode的示例来创建节点

代码演示
class VNode {}
React.createElement = function() {
  return new VNode()
}

单例模式

简介

系统中被唯一使用的

一个类只有一个示例

举个例子:

系统中的登录框,电商平台的购物车

示例

class SingleObject {
  constructor() {
    this.isLogin = false
  }
  login () {
    this.isLogin = true
  }
}

SingleObject.getInstance = (function () {
  let instance
  // 利用闭包把外部函数的变量保存在内存中
  return function () {
    // 如果不存在实例,新建一个实例
    if (!instance) {
      instance = new SingleObject()
    }
    // 如果存在,则直接返回
    return instance
  }
})()

const a = SingleObject.getInstance()
const b = SingleObject.getInstance()
// a进行登录操作
a.login();
// 单例模式,无论创建多少个实例,都是一模一样的
console.log(a === b) // true
// 由于a已登录,b和a一样,b也已登录
console.log(b.isLogin) // true

场景

Vuex的实现

通过传入Vue的实例对象,如果已经传入过,提示已传入实例,否则再将vuex初始化的逻辑写进vue的钩子函数里

代码演示

clipboard.png

vue-router的实现

跟vuex的实现方法大同小异,此处不过多赘述,感兴趣的同学可以查阅vue-router创建时的源码

适配器模式

简介

本来的不适合使用的方法,转成适合的

举个例子:

笔记本电脑通常没有过多插槽,这时需要使用一个转接口进行插槽的扩展

示例

class Iphone {
  getName() {
    return '我是iphone插头'
  }
}


class Target {
  constructor() {
    this.t = new Iphone()
  }
  getName() {
    return `${this.t.getName()},已转接成andorid插头`
  }
}


const target = new Target()
console.log(target.getName()) // 我是iphone插头,已转接成andorid插头

场景

vue中的computed

vue中有时data中的数据不满足于某个需求的使用,通常可以通过计算属性computed重新组装一个新的数据

代码演示
<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

结果:

Original message: "Hello"

Computed reversed message: "olleH"

装饰器模式

(与上方适配器模式不同,上方是完全不能用,这个是增加功能)

简介

为对象添加新功能

不改变原有的结构和功能

将现有对象和装饰器进行分离,两者独立存在

举个例子:

手机壳(拓展手机防摔功能)

示例

class Circle {
  draw () {
    console.log('画圆')
  }
}

class Decorator {
  // 传入circle实例
  constructor(circle) {
    this.circle = circle
  }
  setBorder () {
    console.log('设置边框')
  }
  draw () {
    this.circle.draw()
    // 画圆之后设置边框
    this.setBorder()
  }

}

let c = new Circle()
let decorator = new Decorator(c)
decorator.draw()

场景

ES7装饰器

装饰器可以用来装饰整个类

附上阮一峰老师关于装饰器的文档:es6.ruanyifeng.com/#docs/decor…

代码演示
// @testable是一个装饰器,修改了`MyTestableClass`这个类的行为
@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  // 为它加上了静态属性`isTestable`
  target.isTestable = true;
}

MyTestableClass.isTestable // true
vue-class-component

以class的模式写vue组件

代码演示

clipboar222d.png

代理模式

简介

使用者无权访问目标对象

中间加代理,通过代理做授权控制

举个例子:

公司内网家里访问需要vpn

明星的经纪人

示例

这里通过简单演示vue3响应式的原理进行演示

附上阮一峰老师关于Proxy的文档:es6.ruanyifeng.com/#docs/proxy

const data = {
  name: 'a',
  age: 18,
  likes: []
}

// 为data创建一个代理
const proxyData = new Proxy(data, {
  get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver)
    console.log('get')
    return result
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    console.log('set')
    return result
  }
})

// 通过改变proxyData而不是data,进行代理
proxyData.name = '2'
proxyData.likes.push('eat')
// 打印data
console.log(data) // 'set' 'get' { age: 18, likes: ["eat"], name: "2" }

场景

事件代理(委托)

下面奉上一道经典面试题,如何点击li弹出对应的内容

代码演示
<ul id="ul">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>
window.onload = function(){
  var oUl = document.getElementById("ul");
   // 通过事件冒泡,直接为父组件绑定事件
  oUl.onclick = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if (target.nodeName.toLowerCase() == 'li'){
         alert(target.innerHTML);
    }
  }
}

观察者模式

简介

发布订阅

一对多

举个例子:

去店里点奶茶,点好了被告知取餐

示例

// 主题类
class Subject {
  constructor() {
    // 设置一个state和观察者数组
    this.state = 0
    this.observers = []
  }
  getState() {
    return this.state
  }
  setState(state) {
    this.state = state
    // 赋值的时候调用通知的方法
    this.notify()
  }
  notify() {
    // 通知各个观察者更新
    this.observers.forEach(observer => {
      observer.update()
    })
  }
  // 传入观察者实例,绑定观察者
  attach(observer) {
    this.observers.push(observer)
  }
}

// 观察者类
class Observer {
  constructor(name, subject) {
    this.name = name
    // 传入主题实例
    this.subject = subject
    // 在此观察者实例上传入主题
    this.subject.attach(this)
  }
  update() {
    console.log(`${this.name} update, state:${this.subject.getState()}`)
  }
}

const s = new Subject()

// 一对多 绑定多个观察者
const o1 = new Observer('o1', s)
const o2 = new Observer('o2', s)
s.setState(1234) // "o1 update, state:1234" "o2 update, state:1234"

场景

vue2响应式defineProperty

通过遍历data,给每个属性绑定观察者

代码演示
function update() {
  console.log('更新啦')
}

const newPrototype = Array.prototype
const arrProto = Object.create(newPrototype)
// 由于Object.defineProperty不能监听数组,此处重写数组的方法
// 此处只演示数组的push方法,其他方法的实现与此一致
arrProto.push = function() {
  update()
  newPrototype.push.call(this, ...arguments)
}
function watcherFn(obj) {
  // 如果是数组,重写obj的原型指向arrProto
  if (Array.isArray(obj)) {
    obj.__proto__ = arrProto
  } else {
    for (let k of Object.keys(obj)) {
      register(obj, k, obj[k])
    }
  }
}

function register(obj, key, value) {
  if (typeof value === 'object') {
    watcherFn(value)
  } else {
    Object.defineProperty(obj, key, {
      get() {
        return value
      },
      set(val) {
        if (val !== value) {
          update()
          value = val
        }
      }
    })
  }
}

const obj = {
  name: 'h',
  info: {
    address: 'bj'
  },
  likes: ['music']
}

watcherFn(obj)

obj.name = 'b'
obj.info.address = 'gz'
obj.likes.push('sing')
console.log(obj) // 更新啦 × 3
node自定义事件 EventEmitter
代码演示

clipbzzoard.png

总结

除了以上这六种设计模式外,还有许多其他的设计模式,由于本人是前端开发,在日常工作中遇到的场景大多是这几种设计模式,当然还有状态模式、策略模式等等。

其实学好设计模式,不但能拓展我们编码的思维,还可以在日常的开发中,编写出简洁的、目的明确的、层次分明的代码,更重要的是,我们可以在面试中不时的透露出对设计模式的熟悉与运用,增加面试官对你的好感,最重要的肯定就是为了日后的升职加薪啦,还是那句话:祈祷奇迹不如无尽的练习。希望同学们都可以多模仿,多看源码,减少臭气味的代码,编写出更高质量的代码!

兄台,如果对你有所帮助,请点个赞也是给予我的支持