JavaScript 五大常见函数:数组扁平化、柯里化、防抖、节流与对象拷贝

作者:函数茶馆

时间:2026-10-22

浏览:527

本文深入解析 JavaScript 中五大常见函数面试题与实用技巧,涵盖指定深度的数组扁平化、参数累积的柯里化、控制执行频率的防抖与节流,以及基于递归与 JSON 序列化的对象深拷贝。通过完整代码示例与详细逻辑说明,帮助开发者理解递归、闭包及高阶函数的核心应用,提升前端底层能力。

文章目录

在 JavaScript 开发中,某些经典问题常被反复讨论,且不同开发者往往持有不同的解决思路。要真正透彻理解这些机制,最直观有效的方式便是亲手实现一遍。接下来我们将直接进入核心内容的探讨。

数组扁平化

实现数组扁平化的方式众多,但递归通常被视为最优雅且通用的方案。通过构建一个支持指定深度的扁平化方法,可以掌握处理此类问题的基本范式。


function flattenDepth(array, depth = 1) {
 let result = []
 array.forEach(item => {
 let d = depth
 if (Array.isArray(item) && d > 0) {
  result.push(...(flattenDepth(item, --d)))
 } else {
  result.push(item)
 }
 })
 return result
}
console.log(flattenDepth([1, [2, [3, [4]], 5]])) // [ 1, 2, [ 3, [ 4 ] ], 5 ]
console.log(flattenDepth([1, [2, [3, [4]], 5]], 2)) // [ 1, 2, 3, [ 4 ], 5 ]
console.log(flattenDepth([1, [2, [3, [4]], 5]], 3)) // [ 1, 2, 3, 4, 5 ]

递归实现的核心逻辑简洁明了:遍历数组中的每一项,若当前项为数组且剩余深度大于零,则递归调用自身并传递减一后的深度参数;否则直接将该项加入结果集。由于 depth 参数需对数组中的每一项生效,因此该参数必须置于循环内部进行动态调整。

柯里化

关于函数的柯里化,业界已有大量讨论与多种实现路径。其核心定义可概括为:当传入的参数数量达到预期时执行原函数;若参数不足,则返回一个新函数,并将已传入的参数保存起来,直到累积参数满足要求为止。


function curry(func) {
 var l = func.length
 return function curried() {
 var args = [].slice.call(arguments)
 if(args.length < l) {
  return function() {
  var argsInner = [].slice.call(arguments)
  return curried.apply(this, args.concat(argsInner))
  }
 } else {
  return func.apply(this, args)
 }
 }
}
var f = function(a, b, c) {
 return console.log([a, b, c])
};
var curried = curry(f)
curried(1)(2)(3) // => [1, 2, 3]
curried(1, 2)(3) // => [1, 2, 3]
curried(1, 2, 3) // => [1, 2, 3]

观察上述实现代码可知,关键在于每次调用时判断当前传入参数的个数与原函数期望参数个数的关系。若当前参数少于预期,则继续返回一个接收剩余参数的函数;反之,则使用已保存的参数执行原函数。

防抖

防抖(Debounce)的核心逻辑在于:无论事件被触发多少次,都忽略中间的触发,仅当事件停止触发后,经过指定的时间间隔才执行最后一次操作。基于此理解,我们可以编写一个基础版本的防抖函数。


function debounce(func, wait) {
 var timer
 return function() {
 var context = this
 var args = arguments
 clearTimeout(timer)
 timer = setTimeout(function() {
  func.apply(context, args)
 }, wait)
 }
}

在此基础上,我们增加需求:支持在触发初期立即执行一次(leading),并在触发结束后执行最后一次(trailing),且这些行为需可配置。为了便于测试,我们先构建一个测试页面,通过监听空格键按下使数字累加,以此验证防抖与节流函数的实际效果。


<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
 <style>
  #container{text-align: center; color: #333; font-size: 30px;}
 </style>
</head>
<body>
 <div id="container"></div>
 <script>
  var count = 1
  var container = document.getElementById('container')
  function getUserAction(e) {
  // 空格
  if (e.keyCode === 32) {
   container.innerHTML = count++
  }
  }
  // document.onkeydown = debounce(getUserAction, 1000, false, true)
  document.onkeydown = throttle(getUserAction, 1000, true, true)
  function debounce(func, wait, leading, trailing) {}
  function throttle(func, wait, leading, trailing) {}
 </script>
</body>
</html>

通过引入 leading 和 trailing 两个配置参数,可以精确控制开始和结束时是否执行。若 leading 为 true,则每次按下空格键都会立即执行;若 trailing 为 true,则在等待期结束后执行最后一次触发。以防抖函数为例,若两者均为 true,第一次按下空格会立即执行并计数加 1;随后快速连续按空格时,内部 getUserAction 不会立即执行,而是等待最后一次触发后的指定时间再执行;若 trailing 设为 false,则松手后将不再执行后续操作。


function debounce(func, wait, leading, trailing) {
 var timer, lastCall = 0, flag = true
 return function() {
 var context = this
 var args = arguments
 var now = + new Date()
 if (now - lastCall < wait) {
  flag = false
  lastCall = now
 } else {
  flag = true
 }
 if (leading && flag) {
  lastCall = now
  return func.apply(context, args)
 }
 if (trailing) {
  clearTimeout(timer)
  timer = setTimeout(function() {
  flag = true
  func.apply(context, args)
  }, wait)
 }
 }
}

进一步解释其内部机制:每次调用时记录当前时间,并与上次调用时间对比。若时间差小于设定的间隔,则标记 flag 为 false 并更新时间戳,此时首次执行后后续触发将被忽略;若时间差大于等于间隔,或是在间隔时间之后再次调用,则重置 flag 为 true。读者可对比上述基础版本以加深理解。

节流

节流(Throttle)的核心逻辑在于:无论事件被触发多么频繁,都严格按照指定的时间间隔来执行操作。同样,我们先提供一个基础版本的节流函数实现。


function throttle(func, wait) {
 var timer
 return function() {
 var context = this
 var args = arguments
 if (!timer) {
  timer = setTimeout(function () {
  timer = null
  func.apply(context, args)
  }, wait)
 }
 }
}

与防抖函数类似,节流函数也增加了两个配置参数,并可使用前文提到的测试页面进行验证。从代码结构上看,两者的实现逻辑非常相似,主要区别在于对时间间隔和标志位的处理方式不同。


function throttle(func, wait, leading, trailing) {
 var timer, lastCall = 0, flag = true
 return function() {
 var context = this
 var args = arguments
 var now = + new Date()
 flag = now - lastCall > wait
 if (leading && flag) {
  lastCall = now
  return func.apply(context, args)
 }
 if (!timer && trailing && !(flag && leading)) {
  timer = setTimeout(function () {
  timer = null
  lastCall = + new Date()
  func.apply(context, args)
  }, wait)
 } else {
  lastCall = now
 }
 }
}

对象拷贝

对象拷贝通常分为深拷贝和浅拷贝两种。其中一种较为便捷的“黑科技”手段是利用 JSON 序列化与反序列化,即通过 的方式实现简单的深拷贝。

JSON.parse(JSON.stringify(obj))

另一种更为通用且严谨的方法是使用递归。该方法能够处理更复杂的数据结构,包括循环引用和特殊对象类型。


function clone(value, isDeep) {
 if (value === null) return null
 if (typeof value !== 'object') return value
 if (Array.isArray(value)) {
 if (isDeep) {
  return value.map(item => clone(item, true))
 }
 return [].concat(value)
 } else {
 if (isDeep) {
  var obj = {}
  Object.keys(value).forEach(item => {
  obj[item] = clone(value[item], true)
  })
  return obj
 }
 return { ...value }
 }
}
var objects = { c: { 'a': 1, e: [1, {f: 2}] }, d: { 'b': 2 } }
var shallow = clone(objects, true)
console.log(shallow.c.e[1]) // { f: 2 }
console.log(shallow.c === objects.c) // false
console.log(shallow.d === objects.d) // false
console.log(shallow === objects) // false

具体实现逻辑为:对于基本数据类型直接返回其值;对于引用类型,则遍历其所有属性并递归调用 clone 方法,从而逐层构建新的对象结构。

总结

综上所述,上述几种方法的实现思路主要依赖于递归和高阶函数的运用,其中闭包机制在柯里化、防抖和节流中起到了关键作用。前端开发中常涉及此类底层原理的考察,建议开发者亲自实现一遍,这有助于深入理解其内部机制。希望本文对大家有所帮助,如有任何疑问欢迎留言,小编会及时回复。在此也非常感谢大家对本站网站的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章