导读:本期聚焦于小伙伴创作的《JavaScript递归算法中的数组引用陷阱:如何理解深浅拷贝在集合生成中的应用》,敬请观看详情。在写递归生成子集或排列组合的函数时,不少人会把同一个数组对象反复推入结果集,导致最终所有结果都指向最后一次修改的内容。这其实是对象引用在作怪。JavaScript中数组属于引用类型,赋值或传参不会复制数据本身。递归里若直接复用外部数组,子调用修改会影响父层。用深拷贝或基于索引的浅拷贝策略,才能在每层递归拿到独立集合。本文从引用机制讲清现象成因,给出slice、展开运算符与结构化克隆等可行方案,并比较它们在嵌套结构下的表现差异,帮你在集合生成场景避开共享引用的坑。

用递归生成各种集合是前端和Node开发中很常见的需求,比如求数组的所有子集、全排列,或者把树形结构拍平。很多人在实现时发现,明明每一层递归都往结果里塞了不同的数组,最后打印出来却全都不一样,甚至全部变成了同一份数据。这个问题的核心不在于递归写错,而在于对JavaScript里数组是引用类型理解不够深。

JavaScript递归算法中的数组引用陷阱:如何理解深浅拷贝在集合生成中的应用

一、引用类型在递归中的真实表现

JavaScript的数组、对象都是引用类型。当你把一个数组赋值给另一个变量,或者当作参数传进函数,两个变量指向的是堆内存中的同一个对象。在递归场景里,如果父函数把自己维护的数组直接推进结果集,子函数又继续修改这个数组,那么结果集里存的所有引用都会同步变化。

看一段有问题的代码。下面这个函数想生成输入数组的所有子集,但结果全部错乱:

function subsetsWrong(nums) {
  var result = [];
  var current = [];

  function dfs(start) {
    // 直接把current推进去,存的是引用
    result.push(current);
    for (var i = start; i < nums.length; i++) {
      current.push(nums[i]);
      dfs(i + 1);
      current.pop();
    }
  }

  dfs(0);
  return result;
}

console.log(subsetsWrong([1, 2]));
// 输出类似 [[], [], [], []] 或全是空,因为current最后被pop光了

上面代码中,result.push(current) 放进去的是同一个数组对象的引用。递归结束之后,current 经过一系列 pop 变回空数组,于是 result 里每一项都指向这个空数组。这就是典型的数组引用陷阱。

要避免这个问题,就必须在推入结果时,给每一层递归一个独立的数组副本。这就引出了深浅拷贝的概念:浅拷贝只复制第一层,深拷贝会递归复制所有嵌套结构。

二、浅拷贝在单层集合生成中的用法

如果生成的集合里只有基本类型元素,或者只需要独立的第一层数组,浅拷贝就足够了。JavaScript里常用的浅拷贝方式有 slice()、展开运算符 [...arr],以及 Array.from()

把前面的错误代码改成浅拷贝版本:

function subsetsRight(nums) {
  var result = [];
  var current = [];

  function dfs(start) {
    // 推入当前数组的浅拷贝副本
    result.push(current.slice());
    for (var i = start; i < nums.length; i++) {
      current.push(nums[i]);
      dfs(i + 1);
      current.pop();
    }
  }

  dfs(0);
  return result;
}

console.log(subsetsRight([1, 2]));
// 输出 [[], [1], [1, 2], [2]] 符合预期

这里 current.slice() 创建了一个新数组,内容和 current 当时一样,但地址不同。后续对 current 的 push 和 pop 不会影响已经推入 result 的副本。这种方式在子集、组合类题目中非常高效,因为只需要复制一层。

不过浅拷贝有局限。如果数组里嵌套了对象或其他数组,浅拷贝只复制了外层,内层的引用仍然共享。在生成嵌套集合时,修改其中一个子对象会影响其他副本。

三、深拷贝应对嵌套结构的集合生成

当递归生成的集合包含多层对象,比如克隆树节点路径、生成带有子项的配置组合,就必须用深拷贝。最简单直观的做法是 JSON.parse(JSON.stringify(obj)),但它会丢失函数、undefined、循环引用。

更稳妥的方式是使用现代运行环境中的 structuredClone,或者自己写递归拷贝函数。下面用 structuredClone 演示一个嵌套数组的排列生成:

function generateNestedGroups(items) {
  var result = [];
  var path = [];

  function dfs(index) {
    // 深拷贝当前路径,避免嵌套引用问题
    result.push(structuredClone(path));
    for (var i = index; i < items.length; i++) {
      path.push({ id: items[i], children: [] });
      dfs(i + 1);
      path.pop();
    }
  }

  dfs(0);
  return result;
}

console.log(generateNestedGroups([1, 2]));

在上面的代码里,path 的每一项都是对象,如果用浅拷贝,result 里所有路径中的对象都会指向同一批内存。structuredClone 会连同对象内部一起复制,保证每个结果独立。

需要注意,深拷贝性能开销比浅拷贝大。在元素数量很大、嵌套很深的场景,要权衡是否真的需要深拷贝,或者能否通过重新构造对象来规避引用共享。

四、根据场景选择拷贝策略

实际开发中可以按下面原则判断:

  • 集合元素全是数字、字符串等原始类型:用 slice 或展开运算做浅拷贝即可。
  • 集合包含一层对象但对象内部不再变:浅拷贝通常够用,前提是不要去改对象属性。
  • 集合里的对象在递归中被修改或嵌套多层:使用 structuredClone 或手写深拷贝。
  • 对性能极度敏感且数据规模大:考虑不在递归中存副本,而是用回溯结束时构造新数组。

还有一个常见写法是直接在递归参数里传新数组,这样连拷贝函数都不用显式调用:

function subsetsParam(nums) {
  var result = [];

  function dfs(start, current) {
    result.push(current);
    for (var i = start; i < nums.length; i++) {
      // 每次递归传一个新数组,天然隔离
      dfs(i + 1, current.concat(nums[i]));
    }
  }

  dfs(0, []);
  return result;
}

这种方式利用 concat 返回新数组的特性,把状态通过参数传递,父层完全不持有可变数组,从根源上避免了引用陷阱。代价是会产生更多中间数组,但在多数业务规模下可以接受。

理解引用机制和深浅拷贝差异,是写好递归集合生成逻辑的基础。只要记住:结果集要存副本,不要在多层之间共享同一个可变数组,就能绕开绝大多数坑。

JavaScript递归算法深拷贝修改时间:2026-08-07 17:36:20

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。