JS原理3(函数柯里化、设计模式、防抖、节流)


1.函数柯里化

1.1 概念

函数柯里化:在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化 作为一种高阶技术, 可以提升函数的复用性和灵活性。

函数柯里化 (Currying) 是一种将多个参数的函数转换为单个参数函数的技术。
转换完毕之后的函数只传递函数的一部分参数来调用,并且返回一个新的函数去处理剩下的参数。

例子:

// 调整函数 sum
function sum(num1, num2) {
  return num1 + num2
}

// 改写为 可以实现如下效果
console.log(sum(1)(2))// 
// 1. sum改为接收一个参数,返回一个新函数
// 2. 新函数内部将参数1,参数2累加并返回
function sum(num1) {
  return function (num2) {
    return num1 + num2
  }
}

1.2 求和函数

1.2.1 全局变量

需求:

function sum(a, b, c, d, e) {
  return a + b + c + d + e
}
// 改写函数sum实现:参数传递到5个即可实现累加
// sum(1)(2)(3)(4)(5)
// sum(1)(2,3)(4)(5)
// sum(1)(2,3,4)(5)
// sum(1)(2,3)(4,5)

核心步骤

  1. 定义数组保存参数
  2. 函数接收不定长参数
  3. 调用时将传入的参数,添加到数组中,并判断数组长度:
    1. 满足长度要求:累加并返回结果
    2. 未达到长度要求:继续返回函数本身
let nums = []
function currySum(...args) {
  nums.push(...args)
  if (nums.length >= 5) {
    let res = nums.slice(0, 5).reduce((prev, curv) => prev + curv, 0)
    nums = []
    return res
  } else {
    return currySum
  }
}

1.2.1 使用闭包

需求:

  1. 使用闭包将上一节代码中的全局变量,保护起来
  2. 支持自定义累加的参数个数

闭包:有权访问另一个函数作用域中的变量的函数;一般情况就是在一个函数中包含另一个函数。

function sumMaker(length){
    // 逻辑略
}
// 支持5个累加
const sum5 = sumMaker(5)
// 支持7个累加
const sum7 = sumMaker(7)
sum7(1,2,3)(4,5,6,7)

核心步骤:

  1. 定义函数,接收参数,用来确定参数个数
  2. 内部将上一节的逻辑拷贝进去
  3. 返回原函数
  4. 通过这样的调整,可以让自定义参数的个数,并且没有上一节的全局变量数组
function sumMaker(length) {
  let nums = []
  function inner(...args) {
    nums.push(...args)
    if (nums.length >= length) {
      let res = nums.slice(0, length).reduce((prev, curv) => prev + curv, 0)
      nums = []
      return res
    } else {
      return inner
    }
  }
  return inner
}

1.3 类型判断

需求:
将下列4个类型判断函数,改写为通过函数typeOfTest动态生成

// 有如下4个函数
function isUndefined(thing) {
  return typeof thing === 'undefined'
}
function isNumber(thing) {
  return typeof thing === 'number'
}
function isString(thing) {
  return typeof thing === 'string'
}
function isFunction(thing) {
  return typeof thing === 'function'
}

// 改为通过 typeOfTest 生成:
const typeOfTest =function(){
   // 参数 和 逻辑略

}
const isUndefined = typeOfTest('undefined')
const isNumber = typeOfTest('number')
const isString = typeOfTest('string')
const isFunction = typeOfTest('function')

// 可以通过 isUndefined,isNumber,isString,isFunction 来判断类型:

isUndefined(undefined) // true
isNumber('123') // false
isString('memeda') // true
isFunction(() => { }) // true

核心步骤:

  1. 定义函数,接收需要判断的类型名
  2. 内部返回一个新的函数
    1. 新函数接收需要判断的具体的值
    2. 新函数内部根据外层函数传入的类型,以及传入的值进行判断并返回结果
const typeOfTest = (type) => {
  return (thing) => {
    return typeof thing === type
  }
}

1.4 封装axios请求

需求:
将如下请求函数,变为通过axiosPost函数动态生成

 // 项目开发中不少请求的 请求方法 是相同的,比如
axios({
  url: 'url',
  method: 'get'
})
axios({
  url: 'url',
  method: 'get',
  params: {
    // 
  }
})
axios({
  url: 'url',
  method: 'post',
  data: ''
})
axios({
  url: 'url',
  method: 'post',
  data: '',
  headers: {

  }
})

// 固定请求参数,请求方法固定,其他参数从外部传递进来
requestWithMethod('get')({
  url: '',
  params: {},
  headers: {}
})
requestWithMethod('post')({
  url: '',
  headers: {},
  data: {}
})

核心步骤:

  1. 定义函数,接收请求类型
  2. 函数内部调用axios发请求
function requestWithMethod(method) {
  return (config) => {
    return axios({
      method,
      ...config
    })
  }
}

函数柯里化是一种函数式编程思想:将多个参数的函数转换为单个参数函数,调用时返回新的函数接收剩余参数

2.js设计模式

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

目前说到设计模式,一般指的是《设计模式:可复用面向对象软件的基础》一书中提到的23种常见的软件开发设计模式,JavaScript中不需要生搬硬套这些模式。

2.1 工厂模式

2.1.1 概念

在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)

2.1.2 应用场景

  1. vue3中创建实例的api改为createApp,vue2中是new Vue。 Vue3迁移指南
    • vue3中,没有影响所有Vue实例的api了,全都变成了影响某个app对象的api,比如Vue.component–>app.component
  2. axios.create基于传入的配置,创建一个新的请求对象,可以用来设置多个基地址。axios实例
    // 1. 基于不同基地址创建多个 请求对象
    const request1 = axios.create({
      baseURL: "基地址1"
    })
    const request2 = axios.create({
      baseURL: "基地址2"
    })
    
    // 2. 通过对应的请求对象,调用接口即可
    request1({
      url: '基地址1的接口'
    })
    request2({
      url: '基地址2的接口'
    })
    

2.2 单例模式

2.2.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
  }
}

2.2.2 实际应用

  1. vant组件库中的弹框组件,保证弹框是单例。
    多次弹框,不会创建多个弹框,复用唯一的弹框对象

    1. toast组件:传送门
    2. notify组件:传送门
  2. vue中注册插件,用到了单例的思想(只能注册一次)
    判断插件是否已经注册,已注册,直接提示用户

    1. vue2:传送门
    2. vue3:传送门

2.3 观察者模式

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

  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:
 watch: {
   num(newVal, oldVal) {
	console.log('oldVal:',oldVal)
	console.log('newVal:',newVal)
   }
}

2.4 发布订阅模式

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

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

实现事件总线的核心逻辑

  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移除注册的事件
const bus = new MyEmitter()
// 注册事件
bus.$on('事件名1',回调函数)
bus.$on('事件名1',回调函数)

// 触发事件
bus.$emit('事件名',参数1,...,参数n)

// 移除事件
bus.$off('事件名')

// 一次性事件
bus.$once('事件名',回调函数)

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)
    })
  }
}

2.5 原型模式

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

const student= {
	name: 'xdk'
}
const nstudent = Object.create(student)
cosole.log(nstudent === student) // false

应用
vue2中重写数组方法:
调用方法时(push,pop,shift,unshift,splice,sort,reverse)可以触发视图更新:官方文档源代码

2.6 代理模式

代理模式指的是拦截和控制与目标对象的交互,核心是,通过一个代理对象拦截对原对象的直接操纵。
应用:缓存代理
在这里插入图片描述
核心语法:

  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]
}

2.7 迭代器模式

2.7.1 概念

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

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

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

2.7.2 for in 和for of 区别

  1. for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
    1. 对象默认的属性以及动态增加的属性都是可枚举属性
    2. 遍历出来的是属性名
    3. 继承而来的属性也会遍历
    4. for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环
      1. for of 不会遍历继承而来的属性
      2. 遍历出来的是属性值
Object.prototype.objFunc = function () { }
Array.prototype.arrFunc = 'arrFunc'

const foods = ['土豆', '番茄', '黄瓜']
for (const key in foods) {
  console.log('key', key)
}
// key 0
// key 1
// key 2
// key arrFunc
// key objFunc
for (const iterator of foods) {
  console.log('iterator', iterator)
}
// iterator 土豆
// iterator 番茄
// iterator 黄瓜

2.7.3 可迭代协议和迭代器协议

如何自定义可迭代对象?
需要符合2个协议:可迭代协议和迭代器协议,其实就是按照语法要求实现功能而已。

  1. 可迭代协议:传送门
    1. 给对象增加方法 [Symbol.iterator](){}
    2. 返回一个符合迭代器协议的对象
  2. 迭代器协议:传送门
    1. next方法,返回对象:
      1. {done:true},迭代结束
      2. {done:false,value:'xx'},获取解析并接续迭代
  3. 实现方式
    1. 手写
    2. Generator
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* cityGenerator() {
    //   yield '北京'
    //   yield '上海'
    //   yield '广州'
    // }
    // const city= cityGenerator()
    // return city
  }
}

for (const iterator of obj) {
  console.log('iterator:', iterator)
}

3 防抖

3.1 概念

常见的前端性能优化方案,可以防止JS高频渲染页面时出现的视觉抖动(卡顿):比如

  1. 示例1:页面改变尺寸时,同步调整图表的大小
  2. 示例2:输入内容时,结合ajax进行搜索并渲染结果
    在这里插入图片描述

如果内容的渲染速度过快,都可能会造成抖动效果,并且连带会浪费性能
3. 频繁执行逻辑代码,耗费浏览器性能
4. 频繁发送请求去服务器,耗费服务器性能

适用场景

  1. 在触发频率高的事件中
    • 频率高的事件: resize、input 、scroll 、keyup….
  2. 执行耗费性能操作
    • 耗费性能的操作:操纵页面、网络请求….
  3. 需要实现的效果:连续操作之后只有最后一次生效

这个时候就可以适用防抖来进行优化

3.2 实现防抖

防抖优化之后的效果可以通过一些具体的网站来进行确认,比如12306,他就是通过防抖进行的优化:
在这里插入图片描述

  1. 在输入内容的时候没有发送请求
  2. 输入完毕之后,稍等一会才发送请求去服务器

这就是防抖的效果: 连续事件停止触发后,一段时间内没有再次触发,就执行业务代码

核心步骤

  1. 开启定时器,保存定时器id
  2. 清除已开启的定时器
    在这里插入图片描述

输入框+搜索 例子优化代码如下:

let timeId
document.querySelector('.search-city').addEventListener('input', function () {
  //  2. 清除已开启的定时器
  clearTimeout(timeId)
    
  //  1. 开启定时器,保存定时器id
  timeId = setTimeout(() => {
    renderCity(this.value)
  }, 500)
})

3.3 lodash的debounce方法

实际开发中一般不需要手写防抖,因为已经有库里面提供了对应的方法,可以直接调用,也可以自己手写实现debounce

3.3.1 debounce 方法

lodash工具库中的debounce方法 官方文档

_.debounce(func, [wait=0], [options=])

参数

  1. func (Function):要防抖动的函数。
  2. [wait=0] (number):需要延迟的毫秒数。
  3. [options=] (Object):选项对象。
  4. [options.leading=false] (boolean):指定在延迟开始前调用。
  5. [options.maxWait] (number):设置 func 允许被延迟的最大值。
  6. [options.trailing=true] (boolean):指定在延迟结束后调用。

返回值
(Function):返回新的 debounced(防抖动)函数。

注意:

  1. 实际开发时一般给前2个参数即可,然后适用返回的函数替换原函数即可
  2. 项目中如果有lodash那么直接使用它提供的debounce即可,不仅可以实现防抖,原函数中的this参数均可以正常使用

3.3.2 debounce 实现原理

手写实现debounce函数,实现lodashdebounce方法的核心功能

需求:

  1. 参数:
    1. func (Function): 要防抖动的函数。
    2. [wait=0] (number): 需要延迟的毫秒数。
  2. 返回值:
    1. (Function):返回新的 debounced(防抖动)函数。

核心步骤

  1. 返回防抖动的新函数
  2. 原函数中的this可以正常使用
  3. 原函数中的参数可以正常使用
function debounce(func, wait = 0) {
  let timeId
  // 防抖动的新函数
  return function (...args) {
    let _this = this
    clearTimeout(timeId)
    timeId = setTimeout(function () {
      // 通过apply调用原函数,并指定this和参数
      func.apply(_this, args)
    }, wait)
  }
}

4 节流

4.1 概念

常见的前端性能优化方案,它可以防止高频触发事件造成的性能浪费。
比如:播放视频时同步缓存播放时间,如果要多设备同步,还需要通过ajax提交到服务器

高频触发耗费性能的操作,会造成性能浪费

适用场景:在触发频率高的事件中,执行耗费性能操作,连续触发,单位时间内只有一次生效。

优化之前: 每当触发事件就会执行业务逻辑
在这里插入图片描述
优化之后: 触发事件之后延迟执行逻辑,在逻辑执行完毕之后无法再次触发
在这里插入图片描述

4.2 实现节流

使用节流将播放器记录时间的例子优化:

核心步骤:

  1. 开启定时器,并保存 id
  2. 判断是否已开启定时器
  3. 定时器执行时,id设置为空
// 播放器案例优化之后代码
let timeId
video.addEventListener('timeupdate', function () {
  if (timeId !== undefined) {
    return
  }

  timeId = setTimeout(() => {
    console.log('timeupdate触发')
    localStorage.setItem('currentTime', this.currentTime)
    timeId = undefined
  }, 3000)

})

4.3 lodash的throttle方法

实际开发中一般不需要手写节流,因为已经有库里面提供了对应的方法,可以直接调用,也可以自己手写实现throttle

4.3.1 throttle 方法

lodash工具库中的throttle方法。官方文档

_.throttle(func, [wait=0], [options=])

参数

  1. func (Function):要节流的函数。
  2. [wait=0] (number):需要节流的毫秒。
  3. [options=] (Object):选项对象。
  4. [options.leading=true] (boolean):指定调用在节流开始前。
  5. [options.trailing=true] (boolean):指定调用在节流结束后。

返回值
(Function):返回节流的函数。

注意

  1. 实际开发时一般会给3个参数,然后使用返回的函数替换原函数即可
    • 参数3:options.leading=true默认为true,开始时触发节流函数,一般设置为false
  2. 项目中如果有lodash那么直接使用它提供的throttle即可,不仅可以实现节流,原函数中的this参数均可以正常使用
// 播放器案例使用`lodash` 优化之后的结果如下
const func = function (e) {
  console.log('timeupdate触发')
  console.log('e:', e)
  localStorage.setItem('currentTime', this.currentTime)
}

const throttleFn = _.throttle(func, 1000, { leading: false })

video.addEventListener('timeupdate', throttleFn)

throttle 实现原理

手写实现throttle函数,实现lodashthrottle方法的核心功能

需求:

  1. 参数:
    1. func (Function): 要节流的函数。
    2. [wait=0] (number): 需要节流的毫秒。
  2. 返回值:
    1. (Function):返回节流的函数

核心步骤

  1. 返回节流的新函数
  2. 原函数中的this可以正常使用
  3. 原函数中的参数可以正常使用
// 节流工具函数
function throttle(func, wait = 0) {
  let timeId
  return function (...args) {
    if (timeId !== undefined) {
      return
    }
    const _this = this
    timeId = setTimeout(() => {
      func.apply(_this, args)
      timeId = undefined
    }, wait)
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值