JS进阶9 --JS设计模式


JS进阶8 – 函数柯里化I中我们学习了函数柯里化,并对与其相关的经典面试题和实际应用进行了深入剖析。本文将继续跟大家介绍JS中的第9个重点内容 – JS设计模式。废话不多说,一起来看看吧!

JS进阶9 --JS设计模式

这一节咱们来学习JS中的设计模式
传送门:wiki-设计模式
传送门:JavaScript设计模式与开发实践

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

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

JavaScript中不需要生搬硬套这些模式,咱们结合实际前端开发中的具体应用场景,来看看有哪些常用的设计模式。

这一节咱们会学习:

  1. JS中的常用设计模式
  2. 设计模式在开发/框架中的应用场景

工厂模式

在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>
     <h2>vue2-全局注册组件</h2>
     <div id="app1">
       实例1
       <my-title></my-title>
     </div>
     <div id="app2">
       实例2
       <my-title></my-title>
     </div>
     <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.9/vue.js"></script>
     <script>
       Vue.component('my-title', {
         template: '<h2 style="color:orange">标题组件</h2>'
       })
       const app1 = new Vue({
         el: "#app1"
       })
   
       const app2 = new Vue({
         el: "#app2"
       })
   
     </script>
   </body>
 </html>
  1. 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(){} // 方法
   }
  1. 日常开发中,有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事件绑定
      观察者模式和发布订阅模式的区别也是常见考点,回答方式见下一节

发布订阅模式

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

应用场景

  1. vue2中的EventBus:传送门
  2. vue3中因为移除了实例上对应方法,可以使用替代方案:传送门
    1. 官方推荐,用插件
    2. 我们自己写

手写发布订阅模式

需求
const bus = new MyEmmiter()
// 注册事件
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 MyEmmiter {
    	// 逻辑略
    }

    // 简化 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 MyEmmiter {
  #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>
  <h2>原型模式</h2>
  <div id="app"></div>
  <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)
}
2. 可迭代协议和迭代器协议
  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. 迭代器模式在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高级程序设计
  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值