Skip to content

防抖

  • 浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制
  • 作用是在短时间内多次触发同一个函数,只执行最后一次,或者只在开始时执行
  • 以用户拖拽改变窗口大小,触发 resize 事件为例,在这过程中窗口的大小一直在改变,所以如果我们在 resize 事件中绑定函数,这个函数将会一直触发,而这种情况大多数情况下是无意义的,还会造成资源的大量浪费。
ts
/**
 * @description 函数防抖
 * @param {Function} method 延时调用函数
 * @param {Number} wait 延迟时长
 * @param {Boolean} immediate 立即执行选项
 */
export const Ddebounce = <T extends (...args: any[]) => any>(
  method: T,
  wait: number,
  immediate: boolean
) => {
  if (typeof method !== 'function') {
    throw new TypeError('Expected a function')
  }
  let timeout: ReturnType<typeof setTimeout> | null = null
  // Ddebounce函数为返回值
  // 使用Async/Await处理异步,如果函数异步执行,等待setTimeout执行完,拿到原函数返回值后将其返回
  // args为返回函数调用时传入的参数,传给method
  let Ddebounce = async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    return new Promise((resolve) => {
      // 用于记录原函数执行结果
      let result: ReturnType<T>
      // 将method执行时this的指向设为 debounce 返回的函数被调用时的this指向
      let context = this
      // 如果存在定时器则将其清除
      if (timeout) {
        clearTimeout(timeout)
      }
      // 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
      if (immediate) {
        // 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
        // 这样确保立即执行后wait毫秒内不会被再次触发
        let callNow = !timeout
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
        // 如果满足上述两个条件,则立即执行并记录其执行结果
        if (callNow) {
          result = method.apply(context, args)
          resolve(result)
        }
      } else {
        // 如果immediate为false,则等待函数执行并记录其执行结果
        // 并将Promise状态置为fullfilled,以使函数继续执行
        timeout = setTimeout(() => {
          // args是一个数组,所以使用fn.apply
          // 也可写作method.call(context, ...args)
          result = method.apply(context, args)
          resolve(result)
        }, wait)
      }
    })
  }

  // 在返回的 Ddebounce 函数上添加取消方法
  // Ddebounce.cancel = function () {
  //   clearTimeout(timeout)
  //   timeout = null
  // }
  return Ddebounce
}
javascript
const run = function () {
  console.log("i'm run")
}
window.addEventListener('resize', Ddebounce(run, 2000, false))

节流

  • 节流是指在一段时间内只允许函数执行一次
  • 应用场景如: 输入框的联想、可以限定用户在输入时,只在每两秒钟响应一次联想(或是两秒钟发送一次搜索请求)
ts
/**
 * 节流
 * @param func 回调函数
 * @param delay 延迟时间
 */
export const Dthrottle = <T extends (...args: any[]) => void>(func: T, delay: number) => {
  let timer: ReturnType<typeof setTimeout> | null = null

  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    const context = this

    if (!timer) {
      timer = setTimeout(function () {
        func.apply(context, args)
        timer = null
      }, delay)
    }
  }
}
javascript
// 假设有一个需要被节流的函数
function expensiveOperation() {
  console.log('Running expensive operation...')
  // 这里可以是一些耗时较长的操作
}

// 使用 throttle 函数对 expensiveOperation 进行节流
const throttledFunc = throttle(expensiveOperation, 1000)

// 模拟频繁调用 expensiveOperation 函数
setInterval(function () {
  throttledFunc()
}, 500)

柯里化

  • 函数柯里化的主要目的就是为了减少函数传参,同时将一些固定参数私有化
javascript
const selfCurryFn = (fn) => {
  const fnLen = fn.length
  function curry(...args) {
    const argLen = args.length
    if (argLen < fnLen) {
      function otherCurry(...args2) {
        return curry.apply(this, args.concat(args2))
      }
      return otherCurry
    } else {
      return fn.apply(this, args)
    }
  }
  return curry
}
javascript
const f = (a, b, c) => {
  return console.log([a, b, c])
}

const curried = selfCurryFn(f)
curried(1)(2)(3)

拷贝

typescript
function deepClone<T>(obj: T, clonedMap = new WeakMap()): T {
  // 检查是否已经拷贝过该对象,避免循环引用导致的无限递归
  if (clonedMap.has(obj)) {
    return clonedMap.get(obj) as T
  }

  let newObj: T

  // 特殊类型的处理
  if (obj instanceof Date) {
    newObj = new Date(obj.getTime()) as T
  } else if (obj instanceof RegExp) {
    newObj = new RegExp(obj) as T
  } else if (typeof obj === 'function') {
    // 自定义需要特殊处理的函数类型
    // newObj = obj.bind(null) as T;
    newObj = obj as T
  } else if (Array.isArray(obj)) {
    newObj = [] as unknown as T
  } else if (obj && typeof obj === 'object') {
    newObj = {} as T
  } else {
    return obj
  }

  // 将当前对象添加到已拷贝的对象 Map 中
  clonedMap.set(obj, newObj)

  // 遍历对象的属性进行深拷贝
  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      newObj[key] = deepClone(obj[key], clonedMap)
    }
  }

  return newObj
}
javascript
// 基本类型
const num = 123
const str = 'hello'
const bool = true

console.log(deepClone(num)) // 123
console.log(deepClone(str)) // 'hello'
console.log(deepClone(bool)) // true

// 数组
const arr = [1, 2, [3, 4], { a: 5 }]
const clonedArr = deepClone(arr)

console.log(clonedArr) // [1, 2, [3, 4], { a: 5 }]
console.log(arr === clonedArr) // false

clonedArr[0] = 100
console.log(clonedArr) // [100, 2, [3, 4], { a: 5 }]
console.log(arr) // [1, 2, [3, 4], { a: 5 }]

clonedArr[2][0] = 300
console.log(clonedArr) // [100, 2, [300, 4], { a: 5 }]
console.log(arr) // [1, 2, [300, 4], { a: 5 }]

// 对象
const obj = { x: 1, y: { z: 2 } }
const clonedObj = deepClone(obj)

console.log(clonedObj) // { x: 1, y: { z: 2 } }
console.log(obj === clonedObj) // false

clonedObj.x = 100
console.log(clonedObj) // { x: 100, y: { z: 2 } }
console.log(obj) // { x: 1, y: { z: 2 } }

clonedObj.y.z = 200
console.log(clonedObj) // { x: 100, y: { z: 200 } }
console.log(obj) // { x: 1, y: { z: 200 } }

// 循环引用
const circularObj = { prop: null }
circularObj.prop = circularObj

const clonedCircularObj = deepClone(circularObj)
console.log(clonedCircularObj) // { prop: [Circular] }
console.log(clonedCircularObj.prop === clonedCircularObj) // true

手写 call、apply 及 bind 函数

javascript
Function.prototype.Dcall = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var context = context || window
  context.say = this
  var args = []
  for (let i = 1; i < arguments.length; i++) {
    args.push('arguments[' + i + ']')
  }
  var result = eval('context.say(' + args + ')')
  delete context.say
  return result
}
javascript
Function.prototype.Dapply = function (context, arr) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var context = context || window
  var result
  context.fn = this
  if (!arr) {
    result = context.fn()
  } else {
    var args = []
    for (let i = 0; i < arr.length; i++) {
      args.push('arr[' + i + ']')
    }
    result = eval('context.fn(' + args + ')')
  }
  return result
}
javascript
Function.prototype.Dbind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var self = this
  var args = Array.prototype.slice.call(arguments, 1)
  return function () {
    var bindArgs = Array.prototype.slice.call(arguments)
    return self.apply(context, args.concat(bindArgs))
  }
}
javascript
// 示例函数
function greet() {
  console.log(`Hello, ${this.name}!`)
}

// 1. 使用 Dcall 方法调用函数,并指定上下文对象和参数
const person1 = { name: 'Alice' }
greet.Dcall(person1) // 输出:Hello, Alice!

// 2. 使用 Dapply 方法调用函数,并指定上下文对象和参数数组
const person2 = { name: 'Bob' }
const args = ['Nice to meet you!']
greet.Dapply(person2, args) // 输出:Hello, Bob!

// 3. 使用 Dbind 方法创建新函数,预先绑定上下文和参数
const person3 = { name: 'Charlie' }
const greetPerson3 = greet.Dbind(person3)
greetPerson3() // 输出:Hello, Charlie!

new

在调用 new 的过程中会发生以上四件事情:

  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

实现分析:

  1. 创建一个空对象
  2. 获取构造函数
  3. 设置空对象的原型
  4. 绑定 this 并执行构造函数
  5. 确保返回值为对象
javascript
Function.prototype.Dnew = function (fn, ...args) {
  var obj = Object.create(fn.prototype)
  var ret = fn.apply(obj, args)
  return ret instanceof Object ? ret : obj
}

// 或
Function.prototype.DDnew = function () {
  var obj = {}
  var Constructor = Array.prototype.shift.call(arguments)
  obj.__proto__ = Constructor.prototype
  var result = Constructor.call(obj, arguments)
  return result instanceof Object ? result : obj
}

instanceof

实现原理

就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

javascript
{
  function new_instance_of(leftVaule, rightVaule) {
    let rightProto = rightVaule.prototype
    leftVaule = leftVaule.__proto__
    while (true) {
      if (leftVaule === null) return false
      if (leftVaule === rightProto) return true
      leftVaule = leftVaule.__proto__
    }
  }
}
{
  function new_instance_of(leftVaule, rightVaule) {
    let proto = Object.getPrototypeOf(leftVaule)
    while (true) {
      if (proto == null) return false
      if (proto === rightVaule.prototype) return true
      proto = Object.getPrototypeOf(proto)
    }
  }
}

总结:

使用 typeof 来判断基本数据类型是 ok 的,不过需要注意当用 typeof 来判断 null 类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof,但是 instanceof 也可能判断不准确,比如一个数组,他可以被 instanceof 判断为 Object。所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString.call() 方法