手写JS常见方法 (持续更新中

202 阅读4分钟

深拷贝

普通递归实现对象的深拷贝,很多同学都能写出来,或者使用JSON的parse和stringlfy来实现 但会存在一系列问题,所以有以下改进版本;

  1. 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
  2. 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
  3. 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的隐式原型;
  4. 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
  5. 为什么使用Reflect.ownKeys? 因为以下方法中 只有Reflect.ownKeys可满足需求
    • for in 遍历对象自身和原型链上所有可枚举的属性
    • Object.keys() 返回一个对象自身可枚举属性的数组(不包括原型链上的)
    • Object.getOwnPerprotyNames() 返回一个对象自身所有属性的数组(包括不可枚举,但不包括原型链上的属性)
    • Object.getOwnPerprotySymbol() 返回一个对象自身所有的symbol属性的数组
    • Reflect.ownKeys() = Object.getOwnPerprotyNames() + Object.getOwnPerprotySymbol()
    • Object.getOwnPerprotyDescriptors() 获取对象上所有属性的描述符(包括不可枚举,但不包括原型链上的属性)
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {

  if (obj.constructor === Date) 

  return new Date(obj)       // 日期对象直接返回一个新的日期对象

  if (obj.constructor === RegExp)

  return new RegExp(obj)     //正则对象直接返回一个新的正则对象

  //如果循环引用了就用 weakMap 来解决

  if (hash.has(obj)) return hash.get(obj)
  // 传入参数所有键的特性
  let allDesc = Object.getOwnPropertyDescriptors(obj)

  // 继承原型链

  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

  hash.set(obj, cloneObj)

  for (let key of Reflect.ownKeys(obj)) { 

    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]

  }

  return cloneObj

}

// 下面是验证代码

let obj = {

  num: 0,

  str: '',

  boolean: true,

  unf: undefined,

  nul: null,

  obj: { name: '我是一个对象', id: 1 },

  arr: [0, 1, 2],

  func: function () { console.log('我是一个函数') },

  date: new Date(0),

  reg: new RegExp('/我是一个正则/ig'),

  [Symbol('1')]: 1,

};

Object.defineProperty(obj, 'innumerable', {

  enumerable: false, value: '不可枚举属性' }

);

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))

obj.loop = obj    // 设置loop成循环引用的属性

let cloneObj = deepClone(obj)

cloneObj.arr.push(4)

console.log('obj', obj)

console.log('cloneObj', cloneObj)

防抖

应用场景:

  • 输入框频繁输入内容,搜索或者提交信息
  • 频繁点击按钮
  • 监听浏览器滚动事件,完全某些特定操作
  • 用户缩放浏览器的resize事件
  function debounce(fn, delay, immediate = false, resultCallback) {
    // 定义一个定时器,保存上一次的定时器
    let timer = null
    // 定义一个变量 保存是否执行过fn
    let isInvoke = false
    function _debounce(...args) {
      // 触发时间先判断闭包内的timer是否有值,如果有值则清楚它  这里算是实现防抖的核心了
      if (timer) clearTimeout(timer)
      // 判断是否需要首次执行
      if (immediate && !isInvoke) {
        // 进入函数代表用户需要首次执行,并且isInvoke为false表示没有执行过
        const result = fn.apply(this, args)
        typeof resultCallback === 'function' ? resultCallback(result) : void 0
        // 首次执行完,将变量改为true
        isInvoke = true
      } else {
        // 进入函数代表 非首次执行了
        timer = setTimeout(() => {
          const result = fn.apply(this, args)
          typeof resultCallback === 'function' ? resultCallback(result) : void 0
          // 当成功执行一次就讲isInvoke设为false,这里如果不更改状态,那么程序永远都只有第一次执行才算首次执行了
          // 而我们实际是希望 每一次防抖就要重新计算一次
          // 所以在成功执行了函数后,下次再执行也能首次立即执行
          isInvoke = false
        }, delay);
      }
    }
    // 定义一个方法 取消当前操作
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer)
      timer = null
      isInvoke = false
    }
    return _debounce
  }

节流

  • 监听页面的滚动事件
  • 鼠标移动事件
  • 用户频繁点击按钮
  • 游戏中的一些设计
  function throttle (fn, interval, options = {leading:false, trailing:true}) {
    const { leading, trailing, resultCallback } = options
    // 定义一个变量 记录上一次的时间
    let lastTime = 0
    let timer = null
    function _throttle (...args) {
      // 每次执行函数都获取当前时间
      let nowTime = new Date().getTime()
      // 如果不需要首次执行,一定要在lastTime为0时才将lastTime等于当前时间
      // 如果lastTime不为0时也去赋值,那lastTime就永远都等于nowTime了 不会fn执行函数了
      if (!leading && lastTime === 0) lastTime = nowTime
      // 定义变量得出剩余时间   公式: 剩余时间 = 间隔时间 - (当前时间 - 上一次时间)
      let remainTime = interval - (nowTime - lastTime)
      // 如果剩余时间<=0
      if (remainTime <= 0) {
        // 这里清空是因为,如果程序需要最后一次也执行的时候timer就会有值,如果不把它清空了
        // trailing会执行一次,我们本身的剩余时间<=0也会执行一次
        if (timer) clearTimeout(timer)
        const result = fn.apply(this, args)
        typeof resultCallback === 'function' ? resultCallback(result) : void 0
        // 执行完fn函数后将上一次时间设置为当前时间
        lastTime = nowTime
        // 这里加个return 中断后续执行,如果不中断会接着执行后续代码添加定时器
        return
      }
      // 如果需要最后一次执行,并且没有设置过定时器
      if (trailing && !timer) {
        // 用剩余时间作为最后一次执行的间隔
        timer = setTimeout(() => {
          // 执行函数就把timer清空了,不然我们下次执行就不会再触发最后一次执行了
          timer = null
          // lastTime赋值这里很特别 当本次fn执行完毕后,如果我们是需要首次执行 那么我们需要把lastTime设置成当前时间
          // 这样只要下次再执行超过了interval就会首次执行了
          // 如果不需要首次执行就设置为0 因为当lastTime==0时 我们会将它等于nowTime
          lastTime = leading ? new Date().getTime() : 0
          const result = fn.apply(this, args)
          typeof resultCallback === 'function' ? resultCallback(result) : void 0
        }, remainTime);
      }
    }
    _throttle.cancel = function () {
      // 如果有timer就清空它
      if(timer) clearTimeout(timer)
      timer = null
      lastTime = 0
    }
    return _throttle
  }