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

怎么手写一个递归深拷贝,处理好对象和数组?

先判断类型:原始值、null、函数直接返回;数组新建 [],普通对象新建 {},再遍历自己的属性,对每个值递归调用自身。这样每一层都是新对象,改副本不会影响原数据。

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

基础深拷贝的三步

  1. 直接返回原始值、null 和函数不需要复制,原样返回
  2. 新建容器数组建 [],其它对象建 {}
  3. 递归属性遍历自有可枚举属性,每个值再走一遍深拷贝

基础版把所有对象都当普通对象处理,Date、Set、类的实例拷贝出来都变成了没有原型方法的空壳,不会报错,最难发现。

核心回答

先记住这个答案

深拷贝就是把对象每一层都复制一份,改副本的任何一层都不会影响原对象。写法是递归:值是 null 或者不是对象,就直接返回;是数组就新建 [],是普通对象就新建 {};然后遍历它自己的属性,对每个属性值再调用一次深拷贝。判断时要先排除 null,因为 typeof null 也是 object。这个基础版能处理 JSON 那种纯数据,碰到循环引用、Date、Map 这些就要继续扩展。

  • typeof null 是 object,要先单独排除
  • 用 Array.isArray 决定新建 [] 还是 {}
  • for...in 要配 hasOwnProperty 过滤继承属性
  • 基础版不管 Date、Map、循环引用

递归版的写法和验证

先写出实现,再改副本最里层的值,看原对象有没有被影响。

实现并测试嵌套对象和数组JavaScript
function deepClone(source) {
  if (source === null || typeof source !== 'object') return source // 原始值、null、函数
  const target = Array.isArray(source) ? [] : {}
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = deepClone(source[key]) // 每个值再深拷贝一次
    }
  }
  return target
}

const form = { user: { name: 'Tom', tags: ['a', 'b'] }, count: 1 }
const draft = deepClone(form)
draft.user.name = 'Jerry'
draft.user.tags.push('c')

console.log(JSON.stringify(form))
console.log(JSON.stringify(draft))
console.log(Array.isArray(draft.user.tags), draft.user !== form.user)
查看输出与解释
{"user":{"name":"Tom","tags":["a","b"]},"count":1}
{"user":{"name":"Jerry","tags":["a","b","c"]},"count":1}
true true

副本改了第二层的 name,又往第三层的数组里加了元素,原对象都没变。tags 拷贝后仍然是数组,说明 Array.isArray 那一步生效了。如果这里用 Object.assign({}, form) 做浅拷贝,draft.user 和 form.user 还是同一个对象,改名字会连原对象一起改掉。

每个判断条件为什么这么写

source === null || typeof source !== 'object' 这一行同时处理了三类值。字符串、数字、布尔、undefined、Symbol、BigInt 这些原始值本身不可变,直接返回就是复制。函数的 typeof 是 function,也会直接返回,副本和原对象共用同一个函数,一般业务里这就是想要的效果。null 要写在前面,否则它会被当成对象去新建 {}。

数组为什么要单独判断
如果统一新建 {},数组拷贝出来就变成了 { "0": "a", "1": "b" },没有 length,也不能 map。Array.isArray 是最可靠的判断方式。
for...in 为什么要配 hasOwnProperty
for...in 除了自己的属性,还会遍历原型链上可枚举的属性。不过滤的话,别人往原型上加的属性会被复制成副本自己的属性。直接用 Object.keys(source) 也行,它只返回自有的可枚举属性,效果相同。

基础版处理不了的数据

基础版把所有对象都当成普通对象,遇到内置对象不会报错,而是悄悄得到一个空壳,这是它最危险的地方。

拷贝 Date、Set 和 Symbol 键JavaScript
function deepClone(source) {
  if (source === null || typeof source !== 'object') return source
  const target = Array.isArray(source) ? [] : {}
  for (const key of Object.keys(source)) target[key] = deepClone(source[key])
  return target
}

const id = Symbol('id')
const src = { created: new Date(0), tags: new Set([1, 2]), [id]: 42 }
const copy = deepClone(src)

console.log(copy.created instanceof Date) // 变成了普通对象
console.log(copy.tags.size) // Set 的内容全丢了
console.log(copy[id]) // Symbol 键没被复制
查看输出与解释
false
undefined
undefined

Date 和 Set 的数据存在内部槽里,不是可枚举属性,遍历属性什么也拿不到,所以得到空对象。Object.keys 和 for...in 都不包含 Symbol 键。要支持它们,就要按类型加分支:Date 用 new Date(source),Map 和 Set 新建后逐项深拷贝,Symbol 键用 Reflect.ownKeys 取。

回答前,多想一步

容易答错的地方

只写 typeof source === 'object' 判断
忘了排除 null,null 就会被拷成 {}。原数据里明确表示「没有」的字段,拷贝后变成了空对象,后面的判空逻辑全部失效。
以为 JSON.parse(JSON.stringify(obj)) 等价于深拷贝
它只适合纯 JSON 数据。undefined、函数、Symbol 属性会被丢掉,Date 变成字符串,NaN 和 Infinity 变成 null,Map 和 Set 变成 {},遇到循环引用直接抛 TypeError。
试着用自己的话回答

面试官还会怎么问?

现在有原生的深拷贝吗?

有,structuredClone(value),现代浏览器和 Node.js 都支持。它能正确处理 Date、Map、Set、RegExp 和循环引用,但不能拷贝函数和 DOM 节点,遇到会抛 DataCloneError;类的实例拷贝后也会丢掉原型,变成普通对象。

对象嵌套很深会怎样?

每深一层递归就多一个调用栈帧,层数足够多时会抛 RangeError。业务数据很少深到这个程度,如果数据来自外部、层数不可控,可以改成用栈或队列的迭代写法。

为什么不复制函数?

函数里的代码本身不可变,共享一个引用不会造成数据互相影响,复制它也没有实际意义。真正的问题在于函数里引用的外部变量,这是深拷贝管不到的,所以通常直接返回原函数。

从一道题,走向一组知识

把知识连起来

实用函数实现

怎么用递归手写一个数组扁平化函数?

扁平化和深拷贝都是递归处理嵌套数组,一个要拆掉结构,一个要原样复制结构。

实用函数实现

手写深拷贝时,怎么用 WeakMap 解决循环引用?

基础版遇到对象引用自己会无限递归,下一步就是用 WeakMap 记住拷过的对象。

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

参考资料

  • Object.assign()

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

本题目录
  1. 先记住这个答案
  2. 递归版的写法和验证
  3. 每个判断条件为什么这么写
  4. 基础版处理不了的数据
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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