跳到主要内容

工具函数✅

本主题汇总日常开发中常见的工具函数实现。

实现 lodash debounce ?

/**
* 实现 loadash 的 debounce 函数
* @param {Function} func 要 doubunce 的函数
* @param {number} wait 延迟时间,单位毫秒
* @param {Object} options 可选参数
* @param {boolean} options.leading 是否在延迟开始前调用函数
* @param {boolean} options.trailing 是否在延迟结束后调用函数
* @param {boolean} options.maxWait 最大等待时间,单位毫秒, 避免函数被频繁调用导致一直无法触发,设置一个最大等待时间,确保函数执行
* @returns {Function} 返回一个新的防抖函数
* @example
*
* const debouncedFunc = debounce(() => {
* console.log('Function executed');
* }, 1000, { leading: true, trailing: false });
* debouncedFunc(); // 立即执行
* setTimeout(debouncedFunc, 500); // 不会执行
* setTimeout(debouncedFunc, 1000); // 会执行
*/
module.exports = function debounce (func, wait = 0, options = {}) {

}
答案

debounce 用于限制函数的执行频率,避免在短时间内多次触发同一事件。参考 lodash debounce 函数,实现如下:

/**
 * 实现 loadash 的 debounce 函数
 * @param {Function} func 要 doubunce 的函数
 * @param {number} wait 延迟时间,单位毫秒
 * @param {Object} options 可选参数
 * @param {boolean} options.leading 是否在延迟开始前调用函数
 * @param {boolean} options.trailing 是否在延迟结束后调用函数
 * @param {boolean} options.maxWait 最大等待时间,单位毫秒, 避免函数被频繁调用导致一直无法触发,设置一个最大等待时间,确保函数执行
 * @returns {Function} 返回一个新的防抖函数
 * @example
 * const debouncedFunc = debounce(() => {
 *   console.log('Function executed');
 * }, 1000, { leading: true, trailing: false });
 * debouncedFunc(); // 立即执行
 * setTimeout(debouncedFunc, 500); // 不会执行
 * setTimeout(debouncedFunc, 1500); // 会执行
 */
module.exports = function debounce (func, wait = 0, options = {}) {
  let timeoutId
  let result
  let startTriggerTime
  let thisArg
  const mergeOptions = {
    leading: false,
    trailing: true,
    ...options
  }

  function ReturnDebounce (...args) {
    thisArg = this
    const canCallNow = timeoutId === undefined && mergeOptions.leading
    // 记录首次触发的时间
    if (startTriggerTime === undefined) {
      startTriggerTime = Date.now()
    }

    // debounce 延迟执行逻辑
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      if (mergeOptions.trailing) {
        result = func.apply(thisArg, args)
        startTriggerTime = undefined
      }
      timeoutId = undefined
    }, wait)

    /**
       * leading
       * 且为首次
       * 或者过了超时时间
       * 则可以触发
       */
    if (canCallNow) {
      result = func.apply(thisArg, args)
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => {
        timeoutId = undefined
      }, wait)
    }

    // 注意 maxWait
    if (mergeOptions.maxWait !== undefined && (Date.now() - startTriggerTime) >= mergeOptions.maxWait) {
      result = func.apply(thisArg, args)
      clearTimeout(timeoutId)
      timeoutId = undefined
    }

    return result
  }
  ReturnDebounce.cancel = function () {
    clearTimeout(timeoutId)
    timeoutId = undefined
    startTriggerTime = undefined
  }

  ReturnDebounce.flush = function () {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId)
      timeoutId = undefined
      if (mergeOptions.trailing) {
        result = func.apply(thisArg, arguments)
      }
    }
    return result
  }

  return ReturnDebounce
}

Open browser consoleTests

提示

这个概念实际上来源单片机开发中常见的按键防抖,因为在按下物理按键时,可能由于键盘内部弹簧的机械抖动,导致按键状态在短时间内多次变化,这会导致程序误判为多次按键事件。所以单片机中会通过延时来忽略短时间内的多次按键事件。

答案中包含了 lodash 的全量用例,实现中注意如下细节

  1. lodash 的 debounce 会返回前一次执行的结果
  2. lodash 的返回函数支持 cancel, 和 flush 函数
    • cancel 用于取消防抖函数的执行
    • flush 用于立即执行防抖函数
  3. maxWait 参数用于设置最大等待时间,注意 maxWait 的执行是同步
  4. this 延迟执行后的指向问题

实现 lodash throttle

/**
* 实现 loadash 的 throttle 函数
* @param {Function} func 要 throttle 的函数
* @param {number} wait 等待时间,单位毫秒
* @param {Object} options 可选参数
* @param {boolean} options.leading 是否在开始时调用函数
* @param {boolean} options.trailing 是否在结束时调用函数
* @returns {Function} 返回一个新的节流函数
* @example
*
*/
module.exports = function throttle (func, wait = 0, options = {}) {

}
答案

throttle 用于限制函数的执行频率,对高频执行的函数稳定频率执行。参考 lodash throttle 函数,实现如下:

/**
 * 创建一个防抖函数,该函数会延迟执行 `func`,直到经过了 `wait` 毫秒没有再次调用,
 * 或者直到下一帧浏览器重绘。返回的防抖函数带有 `cancel` 方法用于取消延迟的执行,
 * 以及 `flush` 方法用于立即执行。可以通过 `options` 参数指定是否在延迟开始前
 * (leading)和/或结束后(trailing)调用 `func`。`func` 会以最后一次调用时的参数执行。
 * 多次调用防抖函数会返回最后一次 `func` 执行的结果。
 *
 * **注意:** 如果 `leading` 和 `trailing` 都为 `true`,只有在 `wait` 时间内多次调用
 * 防抖函数时,`func` 才会在延迟结束时执行。
 *
 * 如果 `wait` 为 `0` 且 `leading` 为 `false`,`func` 的执行会被推迟到下一轮事件循环,
 * 类似于 `setTimeout` 的超时为 `0`。
 *
 * 详细区别可参考 [David Corbacho 的文章](https://css-tricks.com/debouncing-throttling-explained-examples/)。
 *
 * @param {Function} func 需要防抖处理的函数
 * @param {number} [wait=0] 延迟的毫秒数
 * @param {Object} [options={}] 配置项
 * @param {boolean} [options.leading=false] 是否在延迟开始前调用
 * @param {number} [options.maxWait] func 允许被延迟的最大时间
 * @param {boolean} [options.trailing=true] 是否在延迟结束后调用
 * @returns {Function} 返回新的防抖函数
 */
function debounce (func, wait = 0, options = {}) {
  let lastArgs
  let lastThis
  let maxWait
  let result
  let timerId
  let lastCallTime

  // 修正:初始化 lastInvokeTime 为 0。用于追踪上一次实际执行函数的时间点,
  // 这对于 throttle 的实现非常关键。原实现的 startTriggerTime 只追踪了一系列调用的开始时间。
  let lastInvokeTime = 0
  let leading = false
  let trailing = true

  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0
  if (typeof options === 'object') {
    leading = !!options.leading
    maxWait = 'maxWait' in options ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }

  function invokeFunc (time) {
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    // 修正:每次实际执行函数时都要更新 lastInvokeTime。
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

  function leadingEdge (time) {
    // 重置任何 maxWait 定时器
    lastInvokeTime = time
    // 启动 trailing 边的定时器
    timerId = setTimeout(timerExpired, wait)
    // 如果需要,立即执行
    return leading ? invokeFunc(time) : result
  }

  function remainingWait (time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxWait === undefined
      ? timeWaiting
      : Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
  }

  // 修正:统一判断是否应该执行的函数。原实现对 leading、maxWait、trailing 的判断分散且容易出错。
  function shouldInvoke (time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime

    // 首次调用,或者距离上次调用已超过 wait,或者系统时间倒退,或者已到达 maxWait
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
         (timeSinceLastCall < 0) || (maxWait !== undefined && timeSinceLastInvoke >= maxWait))
  }

  // 修正:统一的定时器回调,管理所有 trailing 边的逻辑。
  function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }
    // 重新启动定时器
    timerId = setTimeout(timerExpired, remainingWait(time))
  }

  function trailingEdge (time) {
    timerId = undefined

    // 只有在有待执行的 trailing 调用且 trailing 为 true 时才执行
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

  // 修正:cancel 方法现在会重置所有状态变量,确保任何 pending 的执行都被取消
  function cancel () {
    if (timerId !== undefined) {
      clearTimeout(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }

  function flush () {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

  function debounced (...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      if (maxWait !== undefined) {
        // 处理高频调用的情况
        timerId = setTimeout(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined && trailing) {
      timerId = setTimeout(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  return debounced
}

/**
 * 实现 lodash 的 throttle 函数
 * @param {Function} func 需要节流的函数
 * @param {number} wait 等待时间,单位毫秒
 * @param {Object} options 可选参数
 * @param {boolean} options.leading 是否在开始时调用函数
 * @param {boolean} options.trailing 是否在结束时调用函数
 * @returns {Function} 返回一个新的节流函数
 * @example
 *
 */

module.exports = function throttle (func, wait, options = {}) {
  return debounce(func, wait, {
    leading: true,
    trailing: true,
    maxWait: wait,
    ...options
  })
}

Open browser consoleTests

提示

在 lodash 内部,实际上 throttle 是基于 debounce 实现的,主要区别在于 throttle 默认配置会在开始和结束时调用函数,同时 maxWait 参数的值会被设置为 wait 的值。

实现 loadash get

检测对象循环引用

实现字符串过长显示省略号?

实现并发异步调度器

保证同时运行的任务限制。完善代码中 Scheduler 类,使得以下程序能正确输出:

// 实现带并发限制的异步调度器

class Scheduler {
// Your code
}

// 异步任务函数
const fetchUser = (name, delay) => {
return () => new Promise((resolve) => {
setTimeout(() => {
() => console.log(name)
resolve()
}, delay)
})
}
const scheduler = new Scheduler(2) // 控制并发数 2
scheduler.add(fetchUser('A', 2000))
scheduler.add(fetchUser('B', 1000))
scheduler.add(fetchUser('C', 800))
scheduler.add(fetchUser('D', 500))

// 打印顺序: B C A D

class Scheduler {
constructor (concurrency) {
this.concurrency = concurrency
this.tasks = []
this.running = 0
}

add (task) {
return new Promise((resolve) => {
this.tasks.push({
task,
resolve
})
this.schedule()
})
}

schedule () {
while (this.tasks.length > 0 && this.running < this.concurrency) {
const current = this.tasks.shift()
this.running++
current.task().then((result) => {
this.running--
current.resolve(result)
this.schedule()
})
}
}
}

实现 dayjs format 函数

答案
// dayjs format 函数实现
function format (format) {
const date = this

const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hours = date.getHours()
const minutes = date.getMinutes()
const seconds = date.getSeconds()

format = format.replace('YYYY', year)
format = format.replace('MM', month.toString().padStart(2, '0'))
format = format.replace('DD', day.toString().padStart(2, '0'))
format = format.replace('HH', hours.toString().padStart(2, '0'))
format = format.replace('hh', (hours % 12).toString().padStart(2, '0'))
format = format.replace('mm', minutes.toString().padStart(2, '0'))
format = format.replace('ss', seconds.toString().padStart(2, '0'))

return format
}

// 示例用法
const date = new Date()
const formattedDate = date.format('YYYY-MM-DD HH:mm:ss')
console.log(formattedDate) // 输出结果为当前日期和时间的格式化字符串

实现类似 dayjs difference 函数

答案
// dayjs difference 函数实现
function difference (date1, date2, unit = 'day') {
const diffInMs = date2 - date1

switch (unit) {
case 'year':
return diffInMs / (1000 * 60 * 60 * 24 * 365)
case 'month':
return diffInMs / (1000 * 60 * 60 * 24 * 30)
case 'day':
return diffInMs / (1000 * 60 * 60 * 24)
case 'hour':
return diffInMs / (1000 * 60 * 60)
case 'minute':
return diffInMs / (1000 * 60)
case 'second':
return diffInMs / 1000
default:
throw new Error('Unsupported unit for difference calculation')
}
}
// 示例用法
const date1 = new Date('2023-01-01')
const date2 = new Date('2024-01-01')
const diffInDays = difference(date1, date2, 'day')
console.log(`Difference in days: ${diffInDays}`) // 输出结果为 365

实现 lodash isEqual 函数

实现管道函数

答案

实现一个缓存函数

如何做 promise 缓存?上一次调用函数的 promise 没有返回, 那么下一次调用函数依然返回上一个 promise

答案
function cachedPromise (promiseFunction) {
let lastPromise = null

return function () {
// 如果有未完成的 Promise,直接返回
if (lastPromise) return lastPromise

// 创建新的 Promise,并在完成后重置缓存
lastPromise = promiseFunction().finally(() => {
lastPromise = null
})
return lastPromise
}
}

// 示例异步函数
const promiseFunction = () =>
new Promise(resolve => setTimeout(() => resolve('Resolved!'), 2000))

const cachedPromiseFunction = cachedPromise(promiseFunction)

// 多次调用,未完成时返回同一个 Promise
cachedPromiseFunction().then(console.log) // Resolved!
cachedPromiseFunction().then(console.log) // Resolved!

setTimeout(() => {
// 上一次已完成,会返回新的 Promise
cachedPromiseFunction().then(console.log) // Resolved!
}, 3000)

可以用闭包实现 Promise 缓存,让多次调用返回同一个未完成的 Promise。核心思路是用一个变量保存上一次的 Promise,只有当它完成后才会生成新的 Promise。这样可以避免重复请求或重复执行异步操作。

下面是标准实现方式:

function cachedPromise (promiseFunction) {
let lastPromise = null

return function () {
// 如果有未完成的 Promise,直接返回
if (lastPromise) return lastPromise

// 创建新的 Promise,并在完成后重置缓存
lastPromise = promiseFunction().finally(() => {
lastPromise = null
})
return lastPromise
}
}

// 示例异步函数
const promiseFunction = () =>
new Promise(resolve => setTimeout(() => resolve('Resolved!'), 2000))

const cachedPromiseFunction = cachedPromise(promiseFunction)

// 多次调用,未完成时返回同一个 Promise
cachedPromiseFunction().then(console.log) // Resolved!
cachedPromiseFunction().then(console.log) // Resolved!

setTimeout(() => {
// 上一次已完成,会返回新的 Promise
cachedPromiseFunction().then(console.log) // Resolved!
}, 3000)

要点说明:

  • lastPromise 缓存上一次的 Promise。
  • finally 保证 Promise 完成后重置缓存。
  • 这样只有上一次 Promise 未完成时才会复用,否则会重新生成。

常见坑:

  • 不能直接判断 Promise 状态,需用 finally 或第三方库(如 Bluebird)。
  • 适用于防止重复请求、节流等场景。

如需兼容更复杂的状态检测,可考虑引入第三方 Promise 库。

数字千分化的实现方式有哪些?用代码实现一下

答案
方式说明示例代码
toLocaleString使用内置方法,简单可靠1234567..toLocaleString()
正则替换用正则表达式插入逗号'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
48%