小工具知识网

当前位置: 首页 > 行业知识 > 【JavaScript】设计模式

行业知识

【JavaScript】设计模式

2026-11-01 04:32:48 来源:互联网转载

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实例共享,相同的全局设置,可以实现隔离
    ​      Document      #app1,    #app2 {      border: 1px solid #000;    }  ​  

    vue2-全局注册组件

        实例1           实例2              Vue.component('my-title', {      template: '

    标题组件

    '   })    const app1 = new Vue({      el: "#app1"   })​    const app2 = new Vue({      el: "#app2"   })​  ​
  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中的表现形式,返回新对象的函数(方法)

    1. 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. vant的toast和notify组件都用到了单例:多次弹框,不会创建多个弹框,复用唯一的弹框对象
    2. vue中注册插件,vue2和vue3都会判断插件是否已经注册,已注册,直接提示用户

观察者模式

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

举个例子:

  1. dom事件绑定,比如
window.addEventListener('load', () => {  console.log('load触发1')})window.addEventListener('load', () => {  console.log('load触发2')})window.addEventListener('load', () => {  console.log('load触发3')})
  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. 私有属性:#handlers={事件1:[f1,f2],事件2:[f3,f4]}

  3. 实例方法:

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

基础模板:

​      Document​  

自己实现事件总线

 注册事件  触发事件  移除事件  一次性事件注册  一次性事件触发      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', () => {       })  ​
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. 测试一下:
​      Document​  

原型模式

         const app = new Vue({      el: "#app", data: {        foods: ['西瓜', '西葫芦', '西红柿']     }   })    console.log(app.foods.push === Array.prototype.push)  ​​
面试回答:
  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 in 和for of 的区别?

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

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

      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](){} 返回符合 迭代器协议 的对象 *  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  
面试回答:
  1. 迭代器模式在js中有大量的默认实现,因为遍历或者说迭代时日常开发中的高频操作,比如forEach,for in,for of等

  2. for in 和for of的区别:

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

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

      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高级程序设计

上一篇:揭秘剑侠情缘手游杨门奥秘,门派特色与极致玩法体验!

下一篇:怎么清除服务器硬盘数据?