JS - 常用函数的封装(debounce,throttle,deepClone ...)

405 阅读5分钟

写在前面:该文章会持续更新。 主要是一些常用函数的封装,会尽量以大白话来阐述函数的用途以及如何实现等。

所以,各位兄弟姐妹们,收藏走一波吧。

debounce

防抖函数。防抖函数的特点是,在一定时间内当频繁触发某一事件时,只会执行最后一次。举个例子:假如电梯在进入人之后会在10秒后关闭电梯门,在这10秒内如果有人进入,则10秒时间重置,只有10秒内无人进入,则才会执行关门的命令。这个过程中,人进入事件触发,关门则是最终要执行的命令。

那么我们实现如下debounce函数,fn是最终要执行的命令,wait是触发事件之后多少秒执行命令,debounce的return出来的函数则是当事件监听函数(也就是电梯进人之后执行一遍)

   function debounce(fn, wait) {
       var timer = null
       return function(...args) {
           if (timer) {clearTimeout(timer)}
           setTimeout(() => {
               fn(...args)
           }, wait)
       }
   }

防抖函数的处理场景:事件触发较快或者随机,控制执行频率

例子:

  • 假如手指滚动页面的触发频率是1ms,如果不做一些控制,那么滚动函数会频繁处理,可以通过debounce函数控制频率,譬如控制为16ms。

  • 电梯进人之后关门可以通过控制wait来改变进入之后多久关门

  • 页面刷新函数可以加入这个,避免调用者频繁调用页面刷新函数增加负担。

throttle

节流函数,顾名思义,节流函数的目的是节流,就是让水管变细控制单位时间内事件的触发次数。譬如按钮点击,不控制的话可能1分钟内会触发几十次甚至更多的点击事件处理,如果控制为1分钟内只能点击20次,点击一次之后就在3秒内不允许在点击(点击之后不处理事件函数)。

    function throttle(fn, wait) {
       var lastTime = null
       return function(...args) {
          if (!lastTime || (lastTime && new Date().getTime() - lastTime > wait)) {
             fn(...args)
             lastTime = new Date().getTime()
          }
       }
    }

节流函数处理场景:控制单位时间内触发事件的次数

例子:

  • 减少按钮点击次数
  • 可以用来控制绘制帧率

deepClone

```js
    var a = '1'
    var b = 'abc'
    var c = {cc: 'hello'}
    var d = c
    var e = JSON.parse(JSON.stringify(c))
```

我们可以看到上面的代码a和b存储的是值是基础变量也就是直接存储,c变量其实在底层也是有存储值的(这个值是对象{cc:'hello'}的地址,也叫引用)。我们看到d=c其实就是将d的值改为对象{cc:'hello'}的地址,这种就是浅copy。e这种通过JSON.parase来创建了一个新的堆空间,然后里面存储了一个和c长的一样的对象,这种就是深copy。

浅copy我们姑且不说,今天着重说一下深copy的几种实现方式

  • JSON.parase方式
   function deepClone1(val) {
       return JSON.parase(JSON.stringify(val))
   }
   缺点:
   1. function,undefined,Date,RegExp等数据类型不能copy
   2. 循环引用问题会直接爆炸。var a = {a1: '11', a2: a},这种对象就是循环引用对象
  • 递归方式,层层遍历,根据类型来进行赋值
   function deepClone2(val) {
       let copyVal = null
       if (typeof val === 'object' && val !== null) {
           copyVal = Array.isArray(val) ? [] : {}
           for (let key in val) {
             copyVal[key] = deepClone2(val[key])
           }
       } else if (val instanceof Date) { // 可以加很多ifelse判断去处理RegExp等类型的处理
          copyVal = new Date(val)
       } else {
          copyVal = val
       }
       
       return copyVal
   }
  缺点:
  1. 循环引用问题(可以通过WeakMap去解决,不用Map是因为Map可能会引起内存泄漏)
  2. 一些类型不能copy的问题可以在上面代码里加if else判断然后处理,譬如遇到DateRegExp等则直接new一个新的赋值
  

那么咱们通过WeakMap解决一下循环引用的问题

  function cloneFn(val, objMap) {
      let copyVal = null
      if (typeof val === 'object' && val !== null) {
          // 如果要copy的值已经被引用过一次了,那么说明触发了循环引用,则直接把值抛出去即可,不然就进入循环了
          if (objMap.get(val)) {
             return val // return objMap.get(val)
          }
          copyVal = Array.isArray(val) ? [] : {}
          objMap.set(val, copyVal)
          for (let key in val) {
            copyVal[key] = cloneFn(val[key], objMap)
          }
      } else if (val instanceof Date) { // 可以加很多ifelse判断去处理RegExp等类型的处理
         copyVal = new Date(val)
      } else {
         copyVal = val
      }
      
      return copyVal
  }
 
  function deepClone3(val) {
      // 利用闭包,在每次clone的时候都生成一个weakMap,weakMap不要用在全局,不然会出现Map记录混乱的问题,以及循环引用记录不准的问题,导致在某些情况下还是会出现循环引用的问题
      var objMap = new WeakMap()
      return cloneFn(val, objMap)
  }

curry

柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

柯里化不会调用函数。它只是对函数进行转换。

所以curry函数的入参是一个函数,那么return值也是一个函数。

    function curry(fn) {
        return function curried(...args) {
            if (args.length >= fn.length) {
                return fn.apply(this, args)
            } else {
                return function(...args2) {
                    return curried.apply(this, args.concat(args2))
                }
            }
        }
    }

数组拍平

将数组拍成一维数组

function flatten(arr) {
    let r = [] // 最终返回的一维数组
    for (let i = 0; i < arr.length; ++i) {
        if (Arrary.isArray(arr[i])) {
            r = r.concat(flatten[arr[i]])
        } else {
            r.push(arr[i])
        }
    }
    return r
}

function flattenByReduce(arr) {
    return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flattenByReduce(cur) : cur)
    })
}

树结构对象的一维数组表示转换为树结构表示

例:城市列表的转换

/**
[
{
    id: 0,
    pid: null,
    name: 'anhui'
},
{
    id: 1,
    pid: 0,
    name: 'hefei'
},
{
    id: 2,
    pid: 0,
    name: 'huainan'
}
]

转换为

[
{
    id: 0,
    pid: null,
    name: 'anhui'
    children: [
        {
            id: 1,
            pid: 0,
            name: 'hefei'
        },
        {
            id: 2,
            pid: 0,
            name: 'huainan'
        }
    ]
},
]
*/
function arrToTree(arr) {
   // 先将数组数据转换为map映射数据,key为id,value为对象本身
   let map = {}
   arr.map((item) => {
       map[item.id] = item
   })
   
   // result存放多个树节点的根节点(没有pid的则是根节点)
   let result = []
   
   // 遍历数据,找到每个对象的parent节点对象,然后将自己插入到parent的children数组中
   for(let i = 0; i < arr.length; ++i) {
       let pid = arr[i].pid
       let parent = map[pid]
       if (parent) {
           // 如果parent的children数组是空,则初始化该数组
           if (!parent.children) {parent.children = []}
           parent.children.push(arr[i])
       } else { // 没有parent则代表是根节点
           result.push(arr[i])
       }
   }
   return result
}

树结构对象的数结构表示转换为一维数组

例:城市列表的转换

/**
[
{
    id: 0,
    pid: null,
    name: 'anhui'
    children: [
        {
            id: 1,
            pid: 0,
            name: 'hefei'
        },
        {
            id: 2,
            pid: 0,
            name: 'huainan'
        }
    ]
},
]

转换为
[
{
    id: 0,
    pid: null,
    name: 'anhui'
},
{
    id: 1,
    pid: 0,
    name: 'hefei'
},
{
    id: 2,
    pid: 0,
    name: 'huainan'
}
]
*/
function treeToArr(arrTree) {
  
   let r = []
   
   function treeToArr(root, arr) {} {
       arr.push({
           id: root.id,
           pid: root.pid,
           name: root.name
       })
       if (root.children) {
          for (let i = 0; i < root.children.length; ++i) {
              treeToArr(root.children[i], arr)
          }
       }
   }
   
   for (let i = 0; i < arrTree.length; ++i) {
       treeToArr(arrTree[i], r)
   }
   
   return r
}

koa的中间件系统

function compose(middleWares) {
    return function (context) {
        return dispather(0)
        function dispather(i) {
            let fn = middleWares(i)
            if (!fn) {return Promise.resolve()}
            
            try {
                return Promise.resolve(fn(context, dispther.bind(null, i + 1)))
            } catch(err) {
                return Promise.reject(err)
            }
        }
    }
}

function compose(middleWares) {
    return middleWares.reduce((last, cur) => {
        return (context) => {
            cur(context, last)
        }
    }, () => {})
}

EventDispatcher

function EventDispatcher() {
    this.eventMap = {}
}

EventDispatcher.prototype.on = function(type, fn) {
    if (!this.eventMap[type]) {this.eventMap[type] = []}
    const arr = this.eventMap[type]
    arr.push(fn)
    return arr.length - 1
}

EventDispatcher.prototype.off = function(type, id) {
    let arr = this.eventMap[type]
    if (!arr) {
        return
    }
    
    if (arr[i]) {arr[i] = undefined}
}

EventDispatcher.prototype.once = function(type, fn) {
    const id = this.on(type, () => {
        fn()
        this.off(type, id)
    })  
}

EventDispatcher.prototype.dispatch = function(type) {
   let arr = this.eventMap[type]
   if (arr.length) {
       for (let i = 0; i < arr.length; ++i) {
           if (typeof arr[i] === 'function') {
               arr[i]()
           }
       }
   }
}