跳到主要内容

工具函数✅

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

实现 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 的值。

实现 lodash get​

答案

核心概念 lodash.get(object, path, [defaultValue]) 用于安全地获取嵌套对象的属性值。当中间某层属性为 undefined 或 null 时,不会抛出 TypeError,而是返回默认值或 undefined:

  • 路径支持数组形式(如 ['a', '0', 'b'])和字符串形式(如 'a.b[0].c' 或 'a[0].b')。
  • 需先将字符串路径正则解析并标准化为属性数组,随后逐步读取。

手写实现:

function get (object, path, defaultValue) {
if (object === null || object === undefined) {
return defaultValue
}

// 1. 标准化路径:将 a[0].b 或 a.b[0].c 转成 ['a', '0', 'b', 'c']
const pathArray = Array.isArray(path)
? path
: path.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '').split('.')

// 2. 逐层下探取值
let result = object
for (const key of pathArray) {
if (result === null || result === undefined) {
return defaultValue
}
result = result[key]
}

return result === undefined ? defaultValue : result
}

// 验证
const object = { a: [{ b: { c: 3 } }] }
console.log(get(object, 'a[0].b.c')) // 3
console.log(get(object, ['a', '0', 'b', 'c'])) // 3
console.log(get(object, 'a.b.c', 'default')) // 'default'

面试官视角

  • 考查正则提取路径 key 的能力与可选链操作符(?.)底层思想的映射。

延伸阅读

检测对象循环引用​

答案

核心概念 循环引用是指对象的属性直接或间接引用了自身。这会导致深度优先遍历(如深拷贝、JSON.stringify)陷入死循环爆栈:

  • 检测核心思路:在递归遍历对象树时,用一个集合(推荐 Set 或 WeakSet)记录当前路径或整棵树中已访问过的引用对象。
  • 若在遍历子属性时发现该子属性对象已经存在于已访问集合中,则说明存在循环引用。

手写实现:

function hasCycle (obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') {
return false
}

if (seen.has(obj)) {
return true
}

seen.add(obj)

for (const key of Reflect.ownKeys(obj)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
if (hasCycle(obj[key], seen)) {
return true
}
}
}

return false
}

// 验证
const a = {}
const b = { a }
a.b = b
console.log(hasCycle(a)) // true

const clean = { x: 1, y: { z: 2 } }
console.log(hasCycle(clean)) // false

面试官视角

  • 考查图论中环检测的基础思路与在 JS 对象结构中的落地应用。

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

答案

核心概念 字符串截断通常用于 UI 文本溢出展示,常见需求包括:

  1. 纯字符长度截断:超过指定长度直接切片并追加省略符号(如 ...)。
  2. 字符宽度/字节截断:中文字符通常占 2 个字符宽度,英文字符占 1 个。若考虑视觉等宽展示,需根据字符编码(ASCII 与 Unicode)动态计算权重。

手写实现:

function truncate (str, maxLength = 10, omission = '...') {
if (!str || typeof str !== 'string') return ''
if (str.length <= maxLength) return str

// 截取长度需扣除省略符长度
const sliceLen = Math.max(0, maxLength - omission.length)
return str.slice(0, sliceLen) + omission
}

// 支持中英文字节权重的截断
function truncateByWidth (str, maxWidth = 20, omission = '...') {
let width = 0
let index = 0

for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
// 双字节字符判断(中文、全角等)
const charWidth = (code > 255) ? 2 : 1
if (width + charWidth > maxWidth - omission.length) {
break
}
width += charWidth
index = i + 1
}

return index < str.length ? str.slice(0, index) + omission : str
}

// 验证
console.log(truncate('这是一段非常非常长的文本描述内容', 8)) // '这是一段...'
console.log(truncateByWidth('Hello 世界!前端开发面试指南', 14)) // 'Hello 世界...'

面试官视角

  • 考查边界情况处理(如截断长度小于省略号长度)以及国际化场景下中英文混排的宽度差异感知。

实现并发异步调度器​

保证同时运行的任务限制。完善代码中 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 函数​

答案

核心概念 isEqual 用于深度比较两个值是否相等。不同于浅比较(===),它需要递归遍历对象和数组的每个属性,并妥善处理特殊类型:

  1. 基础类型:使用 Object.is 处理 +0 !== -0 及 NaN === NaN。
  2. 类型不一致:若两者的类型不同,直接返回 false。
  3. 内置包装类型:
    • Date:比较时间戳 a.getTime() === b.getTime()。
    • RegExp:比较正则字面量字符串 a.toString() === b.toString()。
  4. 复合类型:
    • 数组与普通对象比较键集合数量,键名不同返回 false,递归比较每个键对应的值。
  5. 循环引用防御:使用 WeakMap 存储已对比过的对象对,防止无限递归爆栈。

手写实现:

function isEqual (value, other, visited = new WeakMap()) {
// 1. 同一引用或严格相等(处理基本类型及 NaN)
if (Object.is(value, other)) {
return true
}

// 2. 其中之一不是对象或为 null,因前面 Object.is 已为 false,故必不相等
if (typeof value !== 'object' || value === null ||
typeof other !== 'object' || other === null) {
return false
}

// 3. 处理循环引用
if (visited.has(value) && visited.get(value) === other) {
return true
}
visited.set(value, other)

// 4. 原型/构造器检查
if (value.constructor !== other.constructor) {
return false
}

// 5. 特殊对象类型对比
if (value instanceof Date) {
return value.getTime() === other.getTime()
}
if (value instanceof RegExp) {
return value.source === other.source && value.flags === other.flags
}

// 6. 数组与对象键名比较
const keysA = Reflect.ownKeys(value)
const keysB = Reflect.ownKeys(other)
if (keysA.length !== keysB.length) {
return false
}

// 7. 递归比较所有键值
for (const key of keysA) {
if (!Reflect.has(other, key) || !isEqual(value[key], other[key], visited)) {
return false
}
}

return true
}

// 验证
console.log(isEqual({ a: [1, 2], b: 3 }, { a: [1, 2], b: 3 })) // true
console.log(isEqual(/abc/g, /abc/g)) // true
console.log(isEqual(NaN, NaN)) // true

面试官视角

  • 考查深度:从基本的递归遍历,下探到特殊类型(NaN、Date、RegExp)的处理,以及用 WeakMap 解决环状引用结构。

延伸阅读

实现管道函数​

答案

核心概念 pipe(管道)函数是函数式编程中的核心构件,它将多个单输入单输出的函数组合为一个新函数,数据流从左到右(从前到后)依次流经每个函数处理,前一个函数的输出作为后一个函数的输入:

  • pipe(f, g, h)(x) 等价于 h(g(f(x)))。
  • 与之相对的 compose 是从右到左执行。
  • 进阶场景需支持异步管道(asyncPipe),即当某个中间环节返回 Promise 时自动 await。

手写实现:

// 同步管道函数
function pipe (...fns) {
return function (initialValue) {
return fns.reduce((acc, fn) => fn(acc), initialValue)
}
}

// 异步管道函数(支持 Promise)
function asyncPipe (...fns) {
return function (initialValue) {
return fns.reduce(
(promise, fn) => promise.then(fn),
Promise.resolve(initialValue)
)
}
}

// 验证
const add1 = x => x + 1
const double = x => x * 2
const square = x => x * x

const compute = pipe(add1, double, square)
console.log(compute(2)) // (2 + 1) * 2 = 6 -> 6^2 = 36

const asyncAdd = async x => x + 10
const asyncCompute = asyncPipe(asyncAdd, double)
asyncCompute(5).then(console.log) // (5 + 10) * 2 = 30

面试官视角

  • 考查函数式编程思想与 Array.prototype.reduce 的灵活运用,能否从容扩展出支持 Promise 的异步管道。

延伸阅读

实现一个缓存函数​

答案

核心概念 缓存函数(记忆化 memoize)将函数的计算结果缓存起来。当以相同的参数再次调用函数时,直接从缓存中读取结果返回,避免重复耗时计算:

  1. 缓存存储:通常使用 Map 或对象存储键值对。
  2. 缓存键生成:默认可根据第一个参数或将所有参数序列化(如 JSON.stringify(args))生成 key,允许用户传入自定义 resolver 生成 key。
  3. 内存控制:长期运行需考虑容量限制与 LRU(最近最少使用)淘汰策略,防止内存无限膨胀。

手写实现:

function memoize (func, resolver) {
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}

const memoized = function (...args) {
const key = resolver ? resolver.apply(this, args) : JSON.stringify(args)
const cache = memoized.cache

if (cache.has(key)) {
return cache.get(key)
}

const result = func.apply(this, args)
cache.set(key, result)
return result
}

memoized.cache = new Map()
return memoized
}

// 验证(斐波那契数列计算加速)
const fib = memoize(function (n) {
if (n <= 1) return n
return fib(n - 1) + fib(n - 2)
})

console.time('fib')
console.log(fib(40)) // 102334155(毫秒级完成)
console.timeEnd('fib')

面试官视角

  • 核心考查闭包缓存机制与缓存键生成策略;进阶加分点在于能指出 JSON.stringify 无法区分部分特殊对象(如循环引用、函数)的缺陷,并提出 LRU 淘汰机制以防内存泄露。

延伸阅读

如何做 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, ',')