• 【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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    应用场景

    1. Vue2->Vue3:

      1. 启用了new Vue,改成了工厂函数createApp-传送门
      2. 任何全局改变 Vue 行为的 API(vue2) 现在都会移动到应用实例上(vue3)
      3. 就不会出现,Vue2中多个Vue实例共享,相同的全局设置,可以实现隔离
      
      
      
      ​
      
        
        
        Document
        
      
      ​
      
        

      vue2-全局注册组件

       
        实例1      
       
        实例2      
         
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    面试回答:
    1. 工厂模式:JS中的表现形式,返回新对象的函数(方法)

      1. 
        function sayHi(){} // 函数
        const obj ={
            name:'jack',
            sayHello(){} // 方法
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
    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
    • 3
    • 4

    核心步骤:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实际应用:

    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')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    核心步骤:

    1. 定义类

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

    3. 实例方法:

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

    基础模板:

    
    
    
    ​
    
      
      
      Document
    
    ​
    
      

    自己实现事件总线

               
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    
    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    面试回答:
    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. 测试一下:
    
    
    
    ​
    
      
      
      Document
    
    ​
    
      

    原型模式

     
        ​ ​
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    面试回答:
    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    代理模式

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

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

    核心语法:

    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    面试回答:
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    1. 可迭代协议:传送门

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

      1. next方法,返回对象:

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

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

        3. 实现方式:

          1. 手写
          2. Generator
    
    // ------------- 迭代协议 -------------
    /**
     * 迭代协议可以定制对象的迭代行为  分为2个协议:
     *  1. 可迭代协议: 增加方法[Symbol.iterator](){} 返回符合 迭代器协议 的对象
     *  2. 迭代器协议: 
     *      有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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    面试回答:
    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高级程序设计
  • 相关阅读:
    基于asp.net+Bootstrap的车牌识别系统
    Matlab在线性代数中的应用(四):相似矩阵及二次型
    PDF中跳转到参考文献后,如何回到原文
    达美乐面试(部分)(未完全解析)
    JavaScript内置对象 - Array数组(三)- 自定义ArrayList
    R语言的计量经济学实践技术应用
    idea创建maven项目
    Java并发编程学习八:ThreadLocal
    ubuntu 安装 mariadb,如何创建用户,并远程连接
    什么是ProxySQL?
  • 原文地址:https://blog.csdn.net/XiugongHao/article/details/136353473