【JavaScript】设计模式

JS设计模式

传送门:wiki-设计模式

传送门:JavaScript设计模式与开发实践

设计模式的指的是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。

目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式。

工厂模式

在JavaScript中,工厂模式的表现形式就是一个直接调用即可返回新对象的函数

// 定义构造函数并实例化
function Dog(name){
    this.name=name
}
const dog = new Dog('柯基')
​
// 工厂模式
function ToyFactory(name,price){
    return {
        name,
        price
    }
}
const toy1 = ToyFactory('布娃娃',10)
const toy2 = ToyFactory('玩具车',15)

应用场景

  1. Vue2->Vue3:

    1. 启用了new Vue,改成了工厂函数createApp-传送门

    2. 任何全局改变 Vue 行为的 API(vue2) 现在都会移动到应用实例上(vue3)

    3. 就不会出现,Vue2中多个Vue实例共享,相同的全局设置,可以实现隔离

      <!DOCTYPE html> <html lang="zh-CN"> ​ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> #app1, #app2 { border: 1px solid #000; } </style> </head> ​ <body>

      vue2-全局注册组件

      实例1 <my-title></my-title>
      实例2 <my-title></my-title>
      <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script> <script> Vue.component('my-title', { template: '

      标题组件

      ' }) const app1 = new Vue({ el: "#app1" }) ​ const app2 = new Vue({ el: "#app2" }) ​ </script> </body> ​ </html>
  2. axios.create:

    1. 基于传入的配置创建一个新的axios实例,传送门
    2. 项目中有2个请求基地址如何设置?

    // 1. 基于不同基地址创建多个 请求对象
    const request1 = axios.create({
    baseURL: "基地址1"
    })
    const request2 = axios.create({
    baseURL: "基地址2"
    })
    const request3 = axios.create({
    baseURL: "基地址3"
    })

    // 2. 通过对应的请求对象,调用接口即可
    request1({
    url: '基地址1的接口'
    })
    request2({
    url: '基地址2的接口'
    })
    request3({
    url: '基地址3的接口'
    })

面试回答:
  1. 工厂模式:JS中的表现形式,返回新对象的函数(方法)

       function sayHi(){} // 函数
       const obj ={
           name:'jack',
           sayHello(){} // 方法
       }
    
  2. 日常开发中,有2个很经典的场景

    1. vue3中创建实例的api改为createApp,vue2中是new Vue

      1. Vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如Vue.component-->app.component
    2. axios.create基于传入的配置,创建一个新的请求对象,可以用来设置多个基地址

单例模式

单例模式指的是,在使用这个模式时,单例对象整个系统需要保证只有一个存在。

需求:

  1. 通过静态方法getInstance获取唯一实例

    const s1 = SingleTon.getInstance()
    const s2 = SingleTon.getInstance()
    console.log(s1===s2)//true

核心步骤:

  1. 定义类

  2. 私有静态属性:#instance

  3. 提供静态方法getInstance:

    1. 调用时判断#instance是否存在:
    2. 存在:直接返回
    3. 不存在:实例化,保存,并返回

    class SingleTon {
    constructor() { }
    // 私有属性,保存唯一实例
    static #instance

    // 获取单例的方法
    static getInstance() {
    if (SingleTon.#instance === undefined) {
    // 内部可以调用构造函数
    SingleTon.#instance = new SingleTon()
    }
    return SingleTon.#instance
    }
    }

实际应用:

  1. vant组件库中的弹框组件,保证弹框是单例

    1. toast组件:传送门

    2. notify组件:传送门

    3. 如果弹框对象

      1. 不存在,-->创建一个新的
      2. 存在,直接用
  2. vue中注册插件,用到了单例的思想(只能注册一次)

    1. vue2:传送门
    2. vue3:传送门
面试回答:
  1. 单例模式:

    1. 保证,应用程序中,某个对象,只能有一个
  2. 自己实现核心为一个返回唯一实例的方法,比如getInstance

    1. 实例存在->返回
    2. 实力不存在->创建,保存->返回
  3. 应用场景:

    1. vanttoastnotify组件都用到了单例:多次弹框,不会创建多个弹框,复用唯一的弹框对象
    2. vue中注册插件,vue2vue3都会判断插件是否已经注册,已注册,直接提示用户

观察者模式

在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

举个例子:

  1. dom事件绑定,比如

    window.addEventListener('load', () => {
    console.log('load触发1')
    })
    window.addEventListener('load', () => {
    console.log('load触发2')
    })
    window.addEventListener('load', () => {
    console.log('load触发3')
    })

  2. Vue中的watch:

面试回答:
  1. 观察者模式重点说清楚2点即可:

    1. 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
    2. 常见场景:vue中的watch,dom事件绑定

观察者模式和发布订阅模式的区别也是常见考点,回答方式见下一节

发布订阅模式01-应用场景

发布订阅模式可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式 )一个没中间商(观察者模式)

应用场景:

  1. vue2中的EventBus:传送门

  2. vue3中因为移除了实例上对应方法,可以使用替代方案:传送门

    1. 官方推荐,用插件
    2. 我们自己写

发布订阅模式02-自己写一个事件总线

需求:

const bus = new HMEmitter()
// 注册事件
bus.$on('事件名1',回调函数)
bus.$on('事件名1',回调函数)
​
// 触发事件
bus.$emit('事件名',参数1,...,参数n)
​
// 移除事件
bus.$off('事件名')
​
// 一次性事件
bus.$once('事件名',回调函数)

核心步骤:

  1. 定义类

  2. 私有属性:#handlers={事件1:[f1,f2],事件2:[f3,f4]}

  3. 实例方法:

    1. $on(事件名,回调函数):注册事件
    2. $emit(事件名,参数列表):触发事件
    3. $off(事件名):移除事件
    4. $once(事件名,回调函数):注册一次性事件

基础模板:

<!DOCTYPE html>
<html lang="zh-CN">
​
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
​
<body>
  <h2>自己实现事件总线</h2>
  <button class="on">注册事件</button>
  <button class="emit">触发事件</button>
  <button class="off">移除事件</button>
  <button class="once-on">一次性事件注册</button>
  <button class="once-emit">一次性事件触发</button>
  <script>
    class HMEmmiter {
        // 逻辑略
    }
​
    // 简化 querySelector调用
    function qs(selector) {
      return document.querySelector(selector)
    }
    // 注册事件
    qs('.on').addEventListener('click', () => {
    
    })
    // 触发事件
    qs('.emit').addEventListener('click', () => {
   
    })
    // 移除事件
    qs('.off').addEventListener('click', () => {
      
    })
    // 一次性事件注册
    qs('.once-on').addEventListener('click', () => {
     
    })
    // 一次性事件触发
    qs('.once-emit').addEventListener('click', () => {
     
    })
  </script>
</body>
​
</html>


class HMEmmiter {
  #handlers = {}
  // 注册事件
  $on(event, callback) {
    if (!this.#handlers[event]) {
      this.#handlers[event] = []
    }
    this.#handlers[event].push(callback)
  }
  // 触发事件
  $emit(event, ...args) {
    const funcs = this.#handlers[event] || []
    funcs.forEach(func => {
      func(...args)
    })
  }
  // 移除事件
  $off(event) {
    this.#handlers[event] = undefined
  }
  // 一次性事件
  $once(event, callback) {
    this.$on(event, (...args) => {
      callback(...args)
      this.$off(event)
    })
  }
}
面试回答:
  1. 发布订阅模式:可以实现的效果类似观察者模式,但是两者略有差异,一句话描述:一个有中间商(发布订阅模式 )一个没中间商(观察者模式)

  2. 经典的场景是vue2中的EventBus,vue3移除了实例的$on,$off,$emit方法,如果还需要使用:

    1. 使用第三方插件
    2. 自己实现事件总线:
  3. 自己实现事件总线的核心逻辑:

    1. 添加类,内部定义私有属性#handlers={},以对象的形式来保存回调函数

    2. 添加实例方法:

      1. $on:

        1. 接收事件名和回调函数
        2. 内部判断并将回调函数保存到#handlers中,以{事件名:[回调函数1,回调函数2]}格式保存
      2. $emit

        1. 接收事件名和回调函数参数
        2. 内部通过#handlers获取保存的回调函数,如果获取不到设置为空数组[]
        3. 然后挨个调用回调函数即可
      3. $off

        1. 接收事件名
        2. #handlers中事件名对应的值设置为undefined即可
      4. $once

        1. 接收事件名和回调函数
        2. 内部通过$on注册回调函数,
        3. 内部调用callback并通过$off移除注册的事件

原型模式

在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型 的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在JavaScript中,Object.create就是实现原型模式的内置api

应用场景:

vue2中重写数组方法:

  1. 调用方法时(push,pop,shift,unshift,splice,sort,reverse)可以触发视图更新:传送门

  2. 源代码:传送门

  3. 测试一下:

    <!DOCTYPE html> <html lang="zh-CN"> ​ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> ​ <body>

    原型模式

    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script> <script> const app = new Vue({ el: "#app", data: { foods: ['西瓜', '西葫芦', '西红柿'] } }) console.log(app.foods.push === Array.prototype.push) </script> ​ </body> ​ </html>
面试回答:
  1. 原型模式:

    1. 基于某个对象,创建一个新的对象
    2. JS中,通过Object.create就是实现了这个模式的内置api
    3. 比如vue2中重写数组方法就是这么做的
  2. vue2中数组重写了7个方法,内部基于数组的原型Array.prototype创建了一个新对象

  3. 创建的方式是通过Object.create进行浅拷贝

  4. 重写的时候:

    1. 调用数组的原方法,获取结果并返回---方法的功能和之前一致
    2. 通知了所有的观察者去更新视图

    const app = new Vue({
    el:"#app",
    data:{
    arr:[1,2,3]
    }
    })
    app.arr.push === Array.prototype.push //false

代理模式

代理模式指的是拦截和控制与目标对象的交互

这里我们来看一个非常经典的代理模式的应用: 缓存代理

核心语法:

  1. 创建对象缓存数据

  2. 获取数据时,先通过缓存判断:

    1. 有直接获取
    2. 没有:调用接口

    // 1. 创建对象缓存数据
    const cache = {}
    async function searchCity(pname) {
    // 2. 判断是否缓存数据
    if (!cache[pname]) {
    // 2.1 没有:查询,缓存,并返回
    const res = await axios({
    url: 'http://hmajax.itheima.net/api/city',
    params: {
    pname
    }
    })
    cache[pname] = res.data.list
    }
    // 2.2 有:直接返回
    return cache[pname]
    }

    document.querySelector('.query').addEventListener('keyup', async function (e) {
    if (e.keyCode === 13) {
    const city = await searchCity(this.value)
    console.log(city)
    }
    })

面试回答:
  1. 代理模式的核心是,通过一个代理对象拦截对原对象的直接操纵

  2. 比如可以通过缓存代理:

    1. 缓存获取到的数据

    2. 拦截获取数据的请求:

      1. 已有缓存:直接返回缓存数据
      2. 没有缓存:去服务器获取数据并缓存
  3. 提升数据获取效率,降低服务器性能消耗

迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示.简而言之就是:遍历

遍历作为日常开发中的高频 操作,JavaScript中有大量的默认实现:比如

  1. Array.prototype.forEach:遍历数组
  2. NodeList.prototype.forEach:遍历dom,document.querySelectorAll
  3. for in
  4. for of

面试题:

  1. for infor of 的区别?

    1. for...in 语句 以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

      1. 对象默认的属性以及动态增加的属性都是可枚举属性
      2. 遍历出来的是属性名
      3. 继承而来的属性也会遍历
    2. for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环

      1. for of不会遍历继承而来的属性
      2. 遍历出来的是属性值

    Object.prototype.objFunc = function () { }
    Array.prototype.arrFunc = 'arrFunc'

    const foods = ['西瓜', '西葫芦', '西兰花']
    for (const key in foods) {
    console.log('for-in:key', key)
    }
    for (const iterator of foods) {
    console.log('for-of:iterator', iterator)
    }

可迭代协议和迭代器协议:

  1. 可迭代协议:传送门

    1. 给对象增加属方法[Symbol.iterator](){}
    2. 返回一个符合迭代器协议的对象
  2. 迭代器协议:传送门

    1. next方法,返回对象:

      1. {done:true},迭代结束

      2. {done:false,value:'xx'},获取解析并接续迭代

      3. 实现方式:

        1. 手写
        2. Generator

    // ------------- 迭代协议 -------------
    /**

    • 迭代协议可以定制对象的迭代行为 分为2个协议:
      1. 可迭代协议: 增加方法Symbol.iterator{} 返回符合 迭代器协议 的对象
      1. 迭代器协议:
    •  有next方法的对象,next方法返回:
      
    •    已结束: {done:true}
      
    •    继续迭代: {done:false,value:'x'}
      
    • 使用Generator
    • 自己实现 对象,next
    • /
      const obj = {
      // Symbol.iterator 内置的常量
      // [属性名表达式]
      Symbol.iterator {

      // ------------- 自己实现 -------------
      const arr = ['北京', '上海', '广州', '深圳']
      let index = 0

      return {
      next() {
      if (index < arr.length) {
      // 可以继续迭代
      return { done: false, value: arr[index++] }
      }
      // 迭代完毕
      return { done: true }
      }
      }


      // ------------- 使用Generator -------------
      // function
      foodGenerator() {
      // yield '西兰花'
      // yield '花菜'
      // yield '西兰花炒蛋'
      // }
      // const food = foodGenerator()
      // return food
      }
      }

      for (const iterator of obj) {
      console.log('iterator:', iterator)
      }
面试回答:
  1. 迭代器模式在js中有大量的默认实现,因为遍历或者说迭代时日常开发中的高频操作,比如forEach,for in,for of

  2. for infor of的区别:

    1. for...in 语句 以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

      1. 对象默认的属性以及动态增加的属性都是可枚举属性
      2. 遍历出来的是属性名
      3. 继承而来的属性也会遍历
    2. for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环

      1. for of不会遍历继承而来的属性
      2. 遍历出来的是属性值
  3. 如何自定义可迭代对象?

    1. 需要符合2个协议:可迭代协议和迭代器协议,其实就是按照语法要求实现功能而已

    2. 可迭代协议:传送门

      1. 给对象增加属方法[Symbol.iterator](){}
      2. 返回一个符合迭代器协议的对象
    3. 迭代器协议:传送门

      1. 有next方法的一个对象,内部根据不同情况返回对应结果:

        1. {done:true},迭代结束
        2. {done:false,value:'xx'},获取解析并接续迭代
      2. 实现方式:

        1. 自己手写实现逻辑
        2. 直接返回一个Generator

参考资料

  1. 阮一峰-《ECMAScript 6 教程》
  2. 图灵社区-JavaScript高级程序设计
相关推荐
我血条子呢几秒前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落7 分钟前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
百事老饼干15 分钟前
Java[面试题]-真实面试
java·开发语言·面试
理想不理想v35 分钟前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记1 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜1 小时前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=1 小时前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++