Skip to content

yayxs/design-patterns-learn

Repository files navigation

JavaScript 设计模式

本文较少包含原生 js 的原型以及原型链的相关内容,以及手写 vue 的发布者-订阅模式。

前言

在前端的圈子里很少有人分享设计模式相关的,东西至于为什么分享这篇,我想我有几点要说的

  • 1 设计模式是大学里的导师才会提及的,在实际的开发中很少用到,会写代码不算的优秀,会组织代码才行
  • 2 面试的时候面试你的不仅仅是前端开发者,一些“上了年纪的”领导基本都会问你设计模式,因为这是他们基本功
  • 3 正是计算机网络和设计模式,操作系统这些核心的本才是区别能否走的长久

好啦,html css javascript 那么本篇就来说一说设计模式

什么是设计模式?

在面向对象的软件设计过程中针对特定问题的简洁优雅的解决方案

或者说是一种编程的套路,让代码更好的维护等

常见的设计模式有哪些?

一般我们所说的设计模式差不多有23 中。比较有js 特色的就是 原型模式 以及 vue 中不识庐山真面目的观察者模式

构造器模式

首先我们来聊一聊第一个设计模式,那就是构造器模式 可能看文章的小伙伴对这个名字有点陌生,让我们把时光拉到小学的时候,或者中学,此时你是班级里的班长,老师班主任的二把手,接着让你出一版班级里的花名册。这时候就是你大展身手的时候了。

  • 学生一 班花 王二花
  • 学生二 班草 李二蛋
  • 等等……

那你说好办

const wangerhua = {}

wangerhua.name = 'weh'
wangerhua.age = 16
wangerhua.sex = '女'

const lierdan = {}

lierdan.name = 'led'
lierdan.age = 15
lierdan.sex = '男'

为了在你的女神三菜 面前耍一手,你熬夜写了班级全部的40 多人,眼看花名册即将完事,你满怀期待的发给你的女神,打算先让她看看……,但是呢,作为旁观者的我们呢,发现是有点问题的,这样熬夜效率不高,虽然也能造出一个花名册,但是此举并不会得到女神的芳心。改进下吧

function Student() {
  this.name = 'wangerhua'
  this.age = '16'
  this.sex = '女'
}
// 然后通过`new` 关键词 活生生的`new` 出 一个王二花

但是现在是写死的肯定不行,把三菜 放在何处对吧,那好办

// 然后通过`new` 关键词 活生生的`new` 出 一个王二花

function Student(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex
}

const 王二花 = new Student('', '', '')
const 李二蛋 = new Student('', '', '')

或者我们使用最舒服的方式es6class 方式

class Student {
  constructor(name, age, sex) {
    this.name = name
    this.age = age
    this.sex = sex
  }
}

工厂模式

其核心目的是为了实现无脑的传参。将创建对象的过程单独封装.较快的生成几类对象

function Persion(name, age, profession, work) {
  this.name = name
  this.age = age
  this.profession = profession
  this.work = work
}
  • 第一个简单的工厂
function Factory(name, age, profession) {
  let work // 是干什么的
  switch (profession) {
    case 'coder':
      work = ['摸鱼']
      break
    case 'coder':
      work = ['摸鱼']
      break
    case 'Fisherman':
      work = ['捕鱼']
    default:
      break
  }
  return new Persion(name, age, profession, work)
}
  • 第二个工厂

    我们设想一个场景,我记得笔者在高二 年级的时候是文理分科的,我想大多在文章的你是理科生,好的那我们就来看一下这个工厂

    // name 你的名字
    // choose 你的选择
    function factory(name, choose) {
      let stu = []
      if (type === '理科生') {
        stu = ['敲代码', '摸鱼']
      } else {
        stu = ['琴棋', '书画']
      }
      return new Student(name, stu)
    }
    
    const yayxs = factory('洋小洋同学', '理科生')

单例模式 The Singleton Pattern

什么是单例模式

什么是单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
对象只需一个(前端中的应用场景):线程池 全局缓存 还有浏览器的`window对象`
优秀的项目:我们的`vuex` 就是应用了单例模式保证全局`store`

实现一个简单的单例模式

实现单例模式的思路:
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建
过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象
/**
 * 单例模式的基本实现 保证一个类仅仅有一个实例
 * 保证一个类仅有一个实例 提供一个访问它的全局访问点
 *
 * 页面中的常见的 全局唯一的弹窗(页面中一般是只显示一个弹窗) 线程池 全局缓存封装 浏览器中 window
 *
 */

function Func(name) {
  this.name = name
  this.instance = null
}

Func.prototype.getName = function () {
  console.log(this.name)
}

// 获取实例

Func.getInstance = function (name) {
  if (!this.instance) {
    this.instance = new Func(name)
  }

  return this.instance
}

使用闭包来实现

/**
 * 使用闭包来实现
 */

class PatternDanli {}
PatternDanli.getInstance = (function () {
  let instance = null

  return function () {
    if (!instance) {
      instance = new PatternDanli()
    }
    return instance
  }
})()

我们还是简单的说一下vuex :单个 vue 应用共享一个 store

let vue
export function install(_vue) {
  if (Vue && _vue === vue) {
    // 首先都是先判断是否已经存在,如果已经存在的话就不重新创建了
    return
  }
  Vue = _vue
}

我们通过上述的的 code 片段,能够得到要想实现单例模式,可以用一个变量来标记这个类是否已经创建过对象,接着完善一下全局缓存

class Storage {
  static getIns() {
    // 判断是否已经new 过
    if (!Storage.instance) {
      Storage.instance = new Storage()
    }
    return Storage.instance
  }
  get(k) {
    return localStorage.getItem(k)
  }
  set(k, val) {
    return localStorage.setItem(k, val)
  }
}

不过注意:在js 中,全局变量不是单例模式

实操:实现一个全局的 Storage

class BaseLocalStorage {
  /**
   * 静态方法判断是否已经 new 过 1个实例
   */
  static getInstance() {
    // 没有new 过
    if (!BaseLocalStorage.instance) {
      BaseLocalStorage.instance = new BaseLocalStorage()
    }

    // 如果已经存在这个唯一的实例直接返回
    return BaseLocalStorage.instance
  }

  // 设置数据 如果value是obj 调用JSON.stringify转换为字符串
  static set(key, value) {
    if (!localStore) {
      return
    }
    let v = value
    try {
      if (typeof value === 'object') {
        v = JSON.stringify(v)
      }
      localStore.setItem(key, v)
    } catch (error) {
      // error
    }
  }
  // 直接读取
  static get(key) {
    if (!localStore) {
      return
    }
    return localStore.getItem(key)
  }
  // 读取转Obj
  static get2Json(key) {
    if (!localStore) {
      return
    }
    const data = localStore.getItem(key)
    if (data) {
      try {
        return JSON.parse(data)
      } catch (error) {
        // todo error
      }
    }

    return null
  }
  // 直接移除
  static remove(key) {
    if (!localStore) {
      return
    }
    try {
      localStore.removeItem(key)
    } catch (error) {
      // todo error
    }
  }
}

策略模式 Strategy Pattern

/**
 * 策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换
 */

const strategies = {
  // 不为空
  isNonEmpty: function (val, errMsg) {
    if (val === '') {
      return errMsg
    }
  },
  // 限制最小长度
  minLen(val, len, errMsg) {
    if (val.length < len) {
      return errMsg
    }
  },
  // 是否是手机号
  isMo(val, errMsg) {
    if (/(^1[3|5|8][0-9]{9}$)/.test(val)) {
      return errMsg
    }
  }
}
/**
 * 校验函数
 */
const validateFunc = function () {
  const v = new Validator() // 创建校验对象
  v.add()
  v.add()
  v.add()

  const errMsg = v.start() // 获得校验结果
}

const formNode = document.getElementById('form')
formNode.onsubmit = function () {
  const errMsg = validateFunc()
  if (errMsg) {
    // 如果errMsg 有返回值 说明校验没有通过
    return false
  }
}

function Validator() {
  this.cache = [] // 保存校验规则
}

Validator.prototype.add = function () {}
Validator.prototype.start = function () {}

原型模式

1、原型模式是设计模式 也是编程范式
2、Object.create 是原型模式的实现
3、语言设计的背景:在 Brendan Eich 为 JavaScript 设计面向对象系统时,借鉴了 Self 和 Smalltalk 这两门基于原型的语言。之所以选择基于原型的面向对象系统,并不是因为时间匆忙,它设计起来相对简单,而 是因为从一开始 Brendan Eich 就没有打算在 JavaScript 中加入类的概念。

接下来我们提到的原型模式不仅仅是一个设计模式,也被称之为一种编程的泛型。

  • 作用适用于创建对象的一种模式

原型模式的实现关键,是语言本身是否提供了 clone 方法。ECMAScript 5 提供了 Object.create方法,可以用来克隆对象。因为本文主要是介绍分享设计模式相关 不会具体分析 js 的api

class Coder1 {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  like() {
    console.log('i like mofish')
  }
}

function Coder2(name, age) {
  this.name = name
  this.age = age
}
Coder2.prototype.like = function () {
  console.log('i like mofish')
}
  • 构造函数Coder2.prototype.constructor===构造函数 Coder2
  • 实例对象coderYayxs.proto // 原型对象 {like: ƒ, constructor: ƒ}

当我试图访问一个 JavaScript 实例的属性/方法时,它首先搜索这个实例本身;当发现实例没有定义对应的属性/方法时,它会转而去搜索实例的原型对象;如果原型对象中也搜索不到,它就去搜索原型对象的原型对象,这个搜索的轨迹,就叫做原型链。

或者说,我们通过es5 的语法来创建一个对象,当然比如当前的学生有个说自己名字的能力,至于这个能力是每个人都拥有的

function Student(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex

  function sayName() {
    console.log(`Iam ${this.name}`)
  }
}

const 王二花 = new Student('', '', '')
const 李二蛋 = new Student('', '', '')

这时候我们可以把通用的能力放在构造函数 的外部,然后类似这样的代码

function Student(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex
  // 引用了sayName函数
  this.sayName = sayName
}

function sayName() {
  console.log(`Iam ${this.name}`)
}

接着我们使用原型的方式,类似上文的程序员 coder 然后把通用的能力(这里暂且这样称之吧) 放在原型上

Student.prototype.sayName = function () {}

精明概要的总结

  • 在 js 中原型是一种范式
  • 具体体现在
function GouZaoFunc() {}
console.log('构造函数的prototype === 构造函数的原型对象', GouZaoFunc.prototype)

const insObj = new GouZaoFunc()

console.log(
  '实例对象的__proto__ === 构造函数的原型对象',
  insObj.__proto__ === GouZaoFunc.prototype
)

console.log(
  '构造函数的原型对象的constructor === 构造函数',
  GouZaoFunc.prototype.constructor === GouZaoFunc
)

装饰器模式(Nest.js 中应用)

它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。

import { Controller, Get } from '@nestjs/common'

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats'
  }
}

适配器模式(axios 中应用)

  • 应用
    • 至于为什么说在axios 中有应用,之所以axios 是如此优秀的一个库,是因为它的 api 足够统一,却带来的在浏览器 以及node 环境 统一的请求能力,这是因为在库中已经做了适配,调用者使用者只需要传入统一化的参数即可
    • 还有就是在vue 的组件化开发中使用计算属性对一些怪异的数据进行转换适配等

代理模式 Proxy Pattern

  • 定义:在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。es6中proxy 一个 Proxy 对象包装另一个对象并拦截诸如读取写入等其他的操作,简言之,拦截对象的操作。为一个对象提供一个代用品或占位符,以便控制对它的访问
  • 场景:图片的懒加载、合并http 请求
  • 其他代理:
    • 防火墙代理
    • 远程代理
    • 保护代理
    • 智能引用
const user = {
  name: 'lee',
  age: 18
}

/**
 * 参数一 target目标对象  要包装的对象(可以是函数)
 * 参数二 handler对象    代理配置 拦截操作方法
 */
const proxy = new Proxy(user, {
  get(user, key) {},
  /**
   * 参数一 target目标对象
   * 参数二 prop目标属性名称
   * 参数三 val 目标属性的值
   * 参数四
   */
  set: function (user, key, val) {}
})

虚拟代理的一种场景,图片的预加载

const loadImg = (function () {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)

  return function (src) {
    imgNode.src = src
  }
})()

const proxyImg = (function () {
  const i = new Image()
  i.onload = function () {
    loadImg(this.src)
  }

  return function (src) {
    loadImg('xxx')
    i.src = src
  }
})()

观察者模式/发布-订阅模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新,有人也称之为发布者订阅模式

/**
 * 发布订阅:又叫做观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的形态发生变化时,所有依赖于它的对象都得到通知
 * 两个重要的角色 发布者 订阅者
 */

/**
 * 发布者
 */
class Publisher {
  constructor() {
    this.observers = []
    console.log('发布者 constructor执行')
  }
  addOne(observer) {
    console.log('发布者 增加订阅者')
    this.observers.push(observer)
  }
  removeOne(observer) {
    console.log('发布者 移除订阅者')
    const len = this.observers.length
    for (let i = 0; i < len; i++) {
      const item = this.observers[i]
      if (item === observer) {
        this.observers.splice(i, 1)
      }
    }
  }
  notifyAll() {
    console.log('发布者 通知所有的订阅者')
    this.observers.forEach((item) => {
      item.update(this)
    })
  }
}

/**
 * 订阅者 被通知 被执行
 */
class Observer {
  constructor() {
    console.log('订阅者 constructor执行')
  }
  update() {
    console.log('订阅者 更新')
  }
}
/**
 * 发布者-订阅模式
 */
class EventEmitter {
  constructor() {
    this.handlers = {} // 存放事件和回调之间的对应关系
    // { 'a' ,[ ()=>{},()=>{}]}
  }

  on(eventName, cb) {
    // 判断map中是否有这个eventName
    if (!this.handlers[eventName]) {
      this.handlers[eventName] = []
    }
    this.handlers[eventName].push(cb)
  }

  emit(eventName, ...args) {
    // 是否有监听函数队列
    if (this.handlers[eventName]) {
      // slice 浅拷贝
      const handlers = this.handlers[eventName].slice()
      handlers.forEach((cb) => {
        cb(...args)
      })
    }
  }

  off(eventName, cb) {
    const cbs = this.handlers[eventName]
    const index = cbs.indexOf(cb)
    if (index !== -1) {
      cbs.splice(index, 1)
    }
  }
}

迭代器模式 Iterator

/**
 * 迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,不需要暴露该对象的内部表示
 * 迭代器分为:内部迭代器和外部迭代器
 * 聚合对象:拥有length属性 可以通过下标访问
 * 迭代器模式:目的性极强的设计模式,解决的问题就是遍历
 */

const Iterator = function (obj) {
  let curr = 0

  let next = function () {
    curr += 1
  }
  let done = function () {
    return curr >= obj.length
  }
  let getCurr = function () {
    return obj[curr]
  }
  return {
    next,
    done,
    getCurr
  }
}

function iteratorGenerator(list) {
  let idx = 0 // 记录当前的索引
  let len = list.length

  return {
    next() {
      let done = idx >= len
      let value = !done ? list[idx++] : undefined

      return {
        done,
        value
      }
    }
  }
}

MDN 有关于 迭代协议 相关的内容

模式与重构

设计模式的目的就是为许多重构行为提供目标

1、提炼函数:把函数的功能点更细粒度

// 每个函数判断一种类型
const toString = Object.prototype.toString
const hasOwnProperty = Object.prototype.hasOwnProperty

const isObject = (value) => {
  return toString.call(value) === '[object Object]'
}

const isArray = (value) => {
  return toString.call(value) === '[object Array]'
}

2、过长的条件分支语句拆成函数

const nameOk = (val) => {
  if (typeof val !== 'string') return false
  const NAME_KW_ARR = ['van', 'lee']
  const nameOk = NAME_KW_ARR.some((name) => val.includes(name))

  return nameOk
}

3、少使用三目运算符 4、使用return 退出多层循环

资料

About

前端 JavaScript 设计模式

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published