工具函数✅
本主题汇总日常开发中常见的工具函数实现。
实现 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 函数,实现如下:
这个概念实际上来源单片机开发中常见的按键防抖,因为在按下物理按键时,可能由于键盘内部弹簧的机械抖动,导致按键状态在短时间内多次变化,这会导致程序误判为多次按键事件。所以单片机中会通过延时来忽略短时间内的多次按键事件。
答案中包含了 lodash 的全量用例,实现中注意如下细节
- lodash 的 debounce 会返回前一次执行的结果
- lodash 的返回函数支持 cancel, 和 flush 函数
- cancel 用于取消防抖函数的执行
- flush 用于立即执行防抖函数
- maxWait 参数用于设置最大等待时间,注意 maxWait 的执行是同步
- 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 函数,实现如下:
在 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 文本溢出展示,常见需求包括:
- 纯字符长度截断:超过指定长度直接切片并追加省略符号(如
...)。 - 字符宽度/字节截断:中文字符通常占 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 用于深度比较两个值是否相等。不同于浅比较(===),它需要递归遍历对象和数组的每个属性,并妥善处理特殊类型:
- 基础类型:使用
Object.is处理+0 !== -0及NaN === NaN。 - 类型不一致:若两者的类型不同,直接返回
false。 - 内置包装类型:
Date:比较时间戳a.getTime() === b.getTime()。RegExp:比较正则字面量字符串a.toString() === b.toString()。
- 复合类型:
- 数组与普通对象比较键集合数量,键名不同返回
false,递归比较每个键对应的值。
- 数组与普通对象比较键集合数量,键名不同返回
- 循环引用防御:使用
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)将函数的计算结果缓存起来。当以相同的参数再次调用函数时,直接从缓存中读取结果返回,避免重复耗时计算:
- 缓存存储:通常使用
Map或对象存储键值对。 - 缓存键生成:默认可根据第一个参数或将所有参数序列化(如
JSON.stringify(args))生成 key,允许用户传入自定义resolver生成 key。 - 内存控制:长期运行需考虑容量限制与 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, ',') |