Skip to content

常见函数手写

普通函数手写

防抖

防抖函数

  1. 高频触发
  2. 耗时操作
  3. 以最后一次执行为准 (例如回城操作,短时间内点了好几次,也只以最后一次为准)
js
/**
 * @description 防抖
 * @param {Function} fn 防抖函数
 * @param {Number} delay 延迟时间
 */
function debounce(fn, delay) {
  let timer = null

  // 返回一个函数,要绑定 this,返回普通函数
  return function (...args) {
    // 每次执行函数前都要清除上一次的定时器
    clearTimeout(timer)
    // 需要使用箭头函数处理 this
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

节流

节流函数

  1. 一段时间内只能执行一次
  2. 可以想象成放技能,技能 CD 的时候,不管按多少次,都没用,只有 CD 好了才会触发
js
/**
 * @description 节流
 * @param {Function} fn 节流函数
 * @param {Number} delay 延迟时间
 */
function throttle(fn, delay) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

柯里化

柯里化

  1. 参数复用
  2. 提前返回
  3. 延迟执行 (不一次性传入所有参数,而是分多次传入,每次传入部分参数,直到参数集齐后执行函数)
  4. 类似 收集七颗龙珠,集齐后才能召唤神龙(执行函数)

问题:实现一个 函数,可以实现 fn(1)(2)(3) = 6

js
// 原函数:需要3个参数
function add(a, b, c) {
  return a + b + c
}

function curry(fn) {
  // 条件1:如果收集的参数数量等于原函数需要的参数数量
  const judge = (...args) => {
    if (args.length === fn.length) {
      // 直接调用 fn
      fn(...args)
    } else {
      // 条件2:参数不足,返回新函数继续收集
      return (...newArgs) => judge(...args, ...newArgs) // 合并已有参数和新参数
    }
  }

  // 返回柯里化后的函数
  return judge
}

const addCurry = curry(add)
console.log(addCurry(1, 2, 3))
console.log(addCurry(1, 2)(3))
console.log(addCurry(1)(2)(3))

可以看到不管怎么调用结果都是 6

20250801142000

call

手写 call 的时候有几点注意事项

  • 首先肯定是在函数原型上去写,这样才能让所有函数使用
  • this 的话传 nullundefined 会指向全局,传原始值就会返回对应的包装类
js
Function.prototype.myCall = function (context, ...args) {
  // 给 this 指向重新赋值
  context = context == null ? globalThis : Object(context)
}

然后解决调用的问题

js
Function.prototype.myCall = function (context, ...args) {
  // 给 this 指向重新赋值
  context = context == null ? globalThis : Object(context)
}

function method(name) {
  console.log(name)
  console.log('this==>', this)
}

method.myCall({ age: 18 }, '张三')

外界是直接 method.myCall 调用,所以myCall 里面的 this 就是 method

js
Function.prototype.myCall = function (context, ...args) {
  // 给 this 指向重新赋值
  context = context == null ? globalThis : Object(context)

  // 拿到函数 fn,这里的 this 就是 fn,因为是 fn.myCall
  const fn = this
}

拿到 fn 后在使用传入 的 context 调用 Fn 就行了

js
Function.prototype.myCall = function (context, ...args) {
  // 给 this 指向重新赋值
  context = context == null ? globalThis : Object(context)

  // 拿到函数 fn,这里的 this 就是 fn,因为是 fn.myCall
  const fn = this

  // 在使用传入的 this 也就是 context 调用 fn
  const res = context.fn(...args) 
  return res 
}

20250807204921

这样运行肯定是报错的。因为此时 context 上面根本就没有 fn,所以需要把 fn 挂载到 context 上,

js
Function.prototype.myCall = function (context, ...args) {
  // 给 this 指向重新赋值
  context = context == null ? globalThis : Object(context)

  // 拿到函数 fn,这里的 this 就是 fn,因为是 fn.myCall
  const fn = this

  context.fn = fn 

  // 在使用传入的 this 也就是 context 调用 fn
  const res = context.fn(...args)
  return res
}

method.myCall({}, '张三')

20250807205241

这样做的话虽然调用成功了,但是是有一些隐患的,因为 this 上面明明穿的是空 {},却打印出来了 fn,所以我们应该使用 Object.definePropertySymbol 来定义 fn,最后拿到函数调用的值后删掉这个符号属性

js
Function.prototype.myCall = function (context, ...args) {
  // 给 this 指向重新赋值
  context = context == null ? globalThis : Object(context)

  // 拿到函数 fn,这里的 this 就是 fn,因为是 fn.myCall
  const fn = this

  const fnKey = Symbol('fnKey')

  Object.defineProperty(context, fnKey, {
    value: fn,
    // 不可枚举,防止被遍历到
    enumerable: false,
  })

  // 在使用传入的 this 也就是 context 调用 fn
  const res = context[fnKey](...args)

  // 拿到结果后,就可以把临时的符号属性删除
  delete context[fnKey]

  return res
}

这样就完整实现了

场景

请求竞态问题

假如我们页面上有两个按钮,这两个按钮是调用同一个函数发送请求,但是他们的参数是不一样的,最终页面上只能展示一个请求的结果,如果我们不对它进行限制,那么就会导致请求竞态问题,假设我们点击按钮1,在按钮1发送的请求没回来的时候,我们又点击按钮2,那么我们很难保证按钮2的请求一定会比按钮1的请求先回来,所以是有可能导致按钮2先回来,那么按钮1的请求结果就会覆盖按钮 2的请求结果,这样就会导致页面上的结果是错误的,所以我们就需要解决这个问题。

js
const getData = id => {
  const time = id === 1 ? 2000 : 1000
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(id)
    }, time)
  })
}

getData(1).then(res => {
  console.log(res)
})

getData(2).then(res => {
  console.log(res)
})

20250801154618

上面这个两次 getData 调用来模拟,我们希望的是最后结果是2,但是由于请求响应的时间是随机的,所以就有可能导致结果是1,这就导致了请求竞态问题。

其实只需要把第一次的 then 取消执行就行了,也就是只执行最后一次的 then 函数。

js
function createCancelTask(asyncTask) {
  return (...args) => {
    asyncTask(...args)
  }
}

const getData = id => {
  const time = id === 1 ? 2000 : 1000
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(id)
    }, time)
  })
}

const getDataTask = createCancelTask(getData)

getDataTask(1).then(res => {
  console.log(res)
})

getDataTask(2).then(res => {
  console.log(res)
})
js
function createCancelTask(asyncTask) {
  return (...args) => {
    // 这里需要返回一个 promise,虽然原函数是promise,
    // 但是不好控制。手动返回一个 promise,做状态穿透就行了
    return new Promise((resolve, reject) => {
      asyncTask(...args).then(
        res => resolve(res),
        err => reject(err)
      )
    })
  }
}

既然这里手动返回的 promise 成功或者失败会走到 asyncTask(...args).then(resolve, reject) then 方法里面,那么只需要把上一次的 resolve 和 reject 置空就行了。也就是执行空函数,就没有影响了。

js
function createCancelTask(asyncTask) {
  return (...args) => {
    // 这里需要返回一个 promise,虽然原函数是promise,
    // 但是不好控制。手动返回一个 promise,做状态穿透就行了
    return new Promise((resolve, reject) => {
      resolve = reject = () => {}
      asyncTask(...args).then(
        res => resolve(res),
        err => reject(err)
      )
    })
  }
}

这样写执行就不会有输出结果了。然后只需要把 resolve = reject = () => {} 保存起来,后面每次调用的时候,都把上一次的 resolvereject 置空就行了。这样当 promise 完成后,执行空函数。

js
function createCancelTask(asyncTask) {
  // 定义一个空函数
  let cancel = () => {}

  return (...args) => {
    // 这里需要返回一个 promise,虽然原函数是promise,
    // 但是不好控制。手动返回一个 promise,做状态穿透就行了
    return new Promise((resolve, reject) => {
      // 将空函数赋值
      cancel = () => {
        resolve = reject = () => {}
      }

      asyncTask(...args).then(
        res => resolve(res),
        err => reject(err)
      )
    })
  }
}

但是有个问题,这个 cancel 函数 什么时候调用呢,其实就是下一次执行的时候就调用,

js
function createCancelTask(asyncTask) {
  // 定义一个空函数
  let cancel = () => {}

  return (...args) => {
    // 这里需要返回一个 promise,虽然原函数是promise,
    // 但是不好控制。手动返回一个 promise,做状态穿透就行了
    return new Promise((resolve, reject) => {
      // 下一次执行的时候就调用,(第一次调用是空函数,无所谓)
      cancel() 

      // 将空函数赋值
      cancel = () => {
        resolve = reject = () => {}
      }

      asyncTask(...args).then(
        res => resolve(res),
        err => reject(err)
      )
    })
  }
}
js
// 最终版

const NOOP = () => {}

function createCancelTask(asyncTask) {
  // 定义一个空函数
  let cancel = NOOP

  return (...args) => {
    // 这里需要返回一个 promise,虽然原函数是promise,
    // 但是不好控制。手动返回一个 promise,做状态穿透就行了
    return new Promise((resolve, reject) => {
      // 下一次执行的时候就调用,(第一次调用是空函数,无所谓)
      cancel()

      // 将空函数赋值
      cancel = () => {
        resolve = reject = NOOP
      }

      asyncTask(...args).then(
        res => resolve(res),
        err => reject(err)
      )
    })
  }
}

20250801161402

这样的话就只有第二次的请求结果了,也就解决了请求竞态的问题。 也就是修改了运行时的函数体

分时函数的封装

当有长任务要执行的时候,导致渲染帧被迫延后了,就没办法进行渲染了,所以看上去会有卡顿现象。

举例:

点击按钮的时候,创建 100000 个 div,

js
const tasks = Array.from({ length: 100000 }, (_, i) => () => {
  const div = document.createElement('div')
  div.textContent = i
  document.body.appendChild(div)
})

btn2.onclick = () => {
  console.log('开始执行任务')
  for (const task of tasks) {
    task()
  }
}

解决: 不能一次性将任务全部执行完,而是应该分步去执行。

渲染帧,每一帧有很多事情做,可能是渲染,执行js,处理事件回调,总之有很多事情要做,做完这些事情过后呢,可能还剩余一部分时间,这一帧16.6ms还没到,这部分时间就是空闲时间,在这一段时间去做处理,不会影响到渲染,别占用太久就行,把这个空闲时间用完,空闲时间可以通过 requestIdleCallback 回调函数来获取。 当有空余时间的时候,就会执行这个回调

封装一个 函数 performTask

js
const tasks = Array.from({ length: 100000 }, (_, i) => () => {
  const div = document.createElement('div')
  div.textContent = i
  document.body.appendChild(div)
})

btn.onclick = () => {
  performTask(tasks)
}

// 分步执行任务
function performTask(tasks) {
  // 渲染帧空闲时间回调
  requestIdleCallback(() => {
    while (当前还有任务要执行 && 这一帧还有空闲时间可用) {
      执行一个任务
    }
  })
}

这样的话一步的执行量就完成了,还得启动下一步的执行。

js
// 分步执行任务
function performTask(tasks) {
  // 渲染帧空闲时间回调
  const _run = () => {
    requestIdleCallback(() => {
      while (当前还有任务要执行 && 这一帧还有空闲时间可用) {
        执行一个任务
      }
      // 当 while 循环结束后,说明当前帧没有空闲时间了,或者任务已经执行完了
      if (当前还有任务要执行) {
        // 继续注册下一步任务事件,重复的调用 _run
        _run()
      }
    })
  }

  _run()
}

这样的话程序的整体结构就写出来了,然后实现代码就行了

js
function performTask(tasks) {
  // 记录当前执行任务的下标
  let index = 0
  // 渲染帧空闲时间回调
  const _run = () => {
    requestIdleCallback(idle => {
      while (index < tasks.length && idle.timeRemaining() > 0) {
        tasks[index++]()
      }
      // 当 while 循环结束后,说明当前帧没有空闲时间了,或者任务已经执行完了
      if (index < tasks.length) {
        // 继续注册下一步任务事件,重复的调用 _run
        _run()
      }
    })
  }

  _run()
}

这样就完成了在 requestIdleCallback 回调函数中去处理这些小任务的处理。接下来可以把这个场景封装成一个通用的场景,比如可以让用户自己决定调用的次数,什么时候调用,调用什么分片任务。

js
/**
 *
 * @param {Number} executorNum 执行任务的次数
 * @param {Function} taskHandler 执行什么任务
 * @param {Function | undefined} scheduler 调度器,默认为 requestIdleCallback
 */
function performTask(executorNum, taskHandler, scheduler) {
  if (scheduler === undefined) {
    // 默认的调度器
    scheduler = isGoOn => {
      requestIdleCallback(idle => {
        isGoOn(() => idle.timeRemaining() > 0)
      })
    }
  }

  // 记录当前执行任务的下标
  let index = 0
  // 渲染帧空闲时间回调
  const _run = () => {
    scheduler(isGoOn => {
      while (index < executorNum && isGoOn()) {
        // 满足条件,执行任务
        taskHandler(index++)
      }
      // 当 while 循环结束后,说明当前帧没有空闲时间了,或者任务已经执行完了
      if (index < executorNum) {
        // 继续注册下一步任务事件,重复的调用 _run
        _run()
      }
    })
  }

  _run()
}

这样就可以这样调用了

js
btn.onclick = () => {
  console.log('开始执行任务')

  const scheduler = isGoOn => {
    let count = 0
    setTimeout(() => {
      isGoOn(() => count++ < 3)
    }, 1000)
  }

  const taskHandler = index => {
    const div = document.createElement('div')
    div.textContent = index
    document.body.appendChild(div)
  }

  // 不传就是 requestIdleCallback
  performTask(100000, taskHandler)
}

异步相关问题

异步函数延迟执行工具

有的时候需要将异步函数延迟执行,但是又不能修改原函数的代码,这个时候就可以使用这个工具函数了,实现也很简单,只需要返回一个 promise,然后将原函数的状态通过 promise 穿透就行了。

js
function delayAsync(fn, delay) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      setTimeout(async () => {
        try {
          const result = await fn.apply(this, args)
          resolve(result)
        } catch (error) {
          reject(error)
        }
      }, delay)
    })
  }
}

使用示例:

js
// 使用示例
const asyncFn = async name => {
  return `Hello, ${name}!`
  // return Promise.reject('error')
}

const delayedAsyncFn = delayAsync(asyncFn, 2000)

delayedAsyncFn('World')
  .then(result => {
    console.log(result) // 2秒后输出: Hello, World!
  })
  .catch(error => {
    console.log('error', error)
  })