前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库JavaScript 深拷贝 手写
前端前端手写题实用函数实现

手写 JavaScript 深拷贝:如何处理普通对象与数组的递归克隆?

深拷贝需递归复制普通对象与数组,关键是先判断类型,对容器类型逐层拷贝,null、函数与原始值直接返回。

前端进阶之旅 · 一题精讲更新于 2026.09.05
前端手写题#实用函数实现#测试#容器化#算法
先看核心答案读代码示例
理解线索

深拷贝核心:类型分派与递归

  1. 直接返回项原始值与函数;typeof 不是 object 且非 null 时直接返回
  2. 引用容器用 Array.isArray 判断后创建 [] 或 {}
  3. 属性遍历for...in 配合 hasOwnProperty 读取复制

只处理普通对象与数组时,此方法足够;遇到循环引用或特殊类型必须扩展。

核心回答

先记住这个答案

手写深拷贝时,先判断值是否为 null 或非对象,是则直接返回;否则用 Array.isArray 判断是数组则创建 [],否则创建 {},然后 for...in 遍历可枚举属性并用 hasOwnProperty 过滤出自有属性,递归调用自身复制每个属性值。这样能保证修改副本不影响原对象,但无法处理循环引用和 Date 等特殊内置对象。

  • typeof null 为 object,需显式判断 null
  • 使用 Array.isArray 区分数组与普通对象
  • 递归复制仅限自有可枚举属性

递归深拷贝的类型判断与复制路径

深拷贝的第一步是区分可直接返回的值与需要递归复制的引用容器。可直接返回的值包括 number、string、boolean、undefined、symbol、bigint 等原始值,以及函数(因为函数通常无需深拷贝,直接共享引用)。null 的 typeof 为 'object',但值本身不可扩展,也应直接返回。因此用 source === null || typeof source !== 'object' 作为直接返回条件,能覆盖以上所有情况。

对于引用值,先判断是否为数组:Array.isArray(source) 为真则返回合法结果数组,否则返回普通对象。然后遍历可枚举键并用 hasOwnProperty 过滤出自有键,递归调用自身复制每个属性值,最终得到全新的结构。递归终止条件是遇到底层原始值,保证每个层级都被新创建。

基础深拷贝实现与三类边界测试JavaScript
function deepClone(source) {
  if (source === null || typeof source !== 'object') return source;
  const target = Array.isArray(source) ? [] : {};
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = deepClone(source[key]);
    }
  }
  return target;
}

// 边界 1:嵌套普通对象
const obj1 = { a: 1, b: { c: 2 } };
const copy1 = deepClone(obj1);
copy1.b.c = 99;
console.log('case1 original:', JSON.stringify(obj1));
console.log('case1 cloned  :', JSON.stringify(copy1));

// 边界 2:数组包含对象与子数组
const obj2 = [1, { x: 2 }, [3, 4]];
const copy2 = deepClone(obj2);
copy2[1].x = 88;
console.log('case2 original:', JSON.stringify(obj2));
console.log('case2 cloned  :', JSON.stringify(copy2));

// 边界 3:原始值、null、undefined 直接返回
const obj3 = { num: 1, nul: null, undef: undefined };
const copy3 = deepClone(obj3);
console.log('case3 original:', JSON.stringify(obj3));
console.log('case3 cloned  :', JSON.stringify(copy3));
console.log('case3 copy.num === obj3.num:', copy3.num === obj3.num);
查看输出与解释
case1 original: {"a":1,"b":{"c":2}}
case1 cloned  : {"a":1,"b":{"c":99}}
case2 original: [1,{"x":2},[3,4]]
case2 cloned  : [1,{"x":88},[3,4]]
case3 original: {"num":1,"nul":null}
case3 cloned  : {"num":1,"nul":null}
case3 copy.num === obj3.num: true

时间 O(n),n 为所有可枚举节点数;空间 O(d),d 为递归调用栈深度。边界 3 中 undefined 属性因 JSON.stringify 被忽略,但实际存在且副本保持相同值。

表单状态复制与隔离更新

假设一个配置编辑器,用户修改某一层级配置后需要“保存副本”用于对比,同时临时草稿状态独立变化。若仅用 Object.assign 浅拷贝,修改内层对象会污染原数据,因此必须深拷贝。使用上述 deepClone 复制 { theme: { color: { primary: '#333' } }, list: [ { id: 1 } ] },得到完全独立的副本。

在副本上修改 theme.color.primary 或新增数组元素,原始对象不受影响。由于递归遍历不引入任何额外依赖,适用于浏览器或 Node 环境。若数据规模在数千层级以内,调用栈安全;但同一层级属性过多不影响。

实现失效的边界与已知限制

当前实现会忽略非枚举属性、Symbol 键以及原型链上的属性,因为 for...in 只遍历可枚举属性且配合 hasOwnProperty 过滤了继承链。访问器属性会读取其 getter 返回值再拷贝,不会保留 setter。Object.create(null) 创建的无原型对象也能复制,但目标对象是 {},丢失了 null 原型。

遇到循环引用时递归会无限展开直至栈溢出,遇到 Date、Map 等内置类型会产生空对象或错误键。解决方案是引入 WeakMap 记录已访问对象,或对每种内置类型独立分支处理。这些扩展会增加实现复杂度,但基础版本已经覆盖了前后端最常见的 JSON 可序列化数据。

回答前,多想一步

容易答错的地方

typeof null 误判为对象
常见错误是不检查 null,直接写 typeof source === 'object',导致 null 被当作普通对象创建空对象。必须加 source === null || 前置判断。
for...in 遍历到原型链属性
忘记 hasOwnProperty 时会把构造器原型上的属性复制进克隆,让副本拥有意外键。应始终使用 Object.prototype.hasOwnProperty.call 过滤。
试着用自己的话回答

面试官还会怎么问?

为什么深拷贝不复制函数?

函数通常无需深拷贝,共享引用即可。大多数深拷贝目标关注数据状态,而非行为。若需隔离函数,须自定义策略,但 JSON 无法表达函数,故常跳过。

嵌套层级很深时会爆栈吗?

递归深度受调用栈限制,典型浏览器约一万层。若预计数据层级极深,应改用迭代加栈实现。不过普通业务数据远低于该阈值。

如何让拷贝保留 Symbol 键?

for...in 无法遍历 Symbol,需用 Object.getOwnPropertySymbols(source) 额外递归复制。这属于基础实现之外的扩展,适用于需要符号键的场景。

从一道题,走向一组知识

把知识连起来

实用函数实现

手写数组扁平化:如何用递归实现任意深度嵌套数组的拍平?

同属「实用函数实现」专题,接着看 数组扁平化 手写 flat 在具体场景中的处理方式。

实用函数实现

手写深拷贝时如何用 WeakMap 解决循环引用问题?

同属「实用函数实现」专题,接着看 深拷贝 循环引用 WeakMap 在具体场景中的处理方式。

放回知识体系中复习JavaScript 进阶知识

参考资料

  • Object.assign()

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 递归深拷贝的类型判断与复制路径
  3. 表单状态复制与隔离更新
  4. 实现失效的边界与已知限制
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑