前端八股:JavaScript 深拷贝详解:实现方式、递归原理与循环引用

JavaScript 深拷贝详解:实现方式、递归原理与循环引用

在 JavaScript 中,深拷贝是前端面试的高频知识点,主要考察基本类型与引用类型、浅拷贝和深拷贝的区别,以及递归和 WeakMap 的使用。

理解深拷贝,首先需要知道 JavaScript 中对象的赋值机制。

什么是浅拷贝和深拷贝?

JavaScript 中的对象属于引用类型。当一个变量赋值给另一个变量时,如果赋的是对象,复制的是对象的引用,而不是重新创建一个对象。

例如:

javascript 复制代码
const obj1 = {
  name: "Tom",
  info: {
    age: 18
  }
}

const obj2 = obj1

obj2.info.age = 20

console.log(obj1.info.age)
// 20

这是因为 obj1 和 obj2 指向同一个对象,因此修改其中一个,另一个也会受到影响。

浅拷贝(Shallow Copy) 会创建一个新对象,但只复制第一层属性。如果属性值是嵌套对象,复制的仍然是原对象的引用。

javascript 复制代码
const obj1 = {
  name: "Tom",
  info: { age: 18 }
}

const obj2 = { ...obj1 }

console.log(obj1 === obj2)
// false

console.log(obj1.info === obj2.info)
// true

虽然 obj1 和 obj2 是两个不同对象,但里面的 info 仍然指向同一个对象。

而深拷贝(Deep Clone) 会递归复制嵌套对象,使新旧对象之间不共享需要被克隆的嵌套引用。

javascript 复制代码
const obj2 = structuredClone(obj1)

console.log(obj1 === obj2)
// false

console.log(obj1.info === obj2.info)
// false

因此,浅拷贝和深拷贝最核心的区别是:

浅拷贝只复制第一层,深拷贝还会继续复制内部嵌套的对象。

需要注意,深拷贝并不是把所有值都复制成新对象。数字、字符串、布尔值等基本类型可以直接复制。


JavaScript 深拷贝有哪些实现方式?

常见的深拷贝方式主要有四种:

实现方式 优点 局限
JSON.parse(JSON.stringify()) 简单,容易使用 不支持循环引用,会丢失部分类型信息
structuredClone() 原生 API,支持多种内置类型和循环引用 不支持函数,部分特殊对象不能克隆
_.cloneDeep() Lodash 提供,功能比较完善 需要引入第三方库
手写递归 灵活、可定制,面试常考 需要处理不同类型和循环引用

其中,最值得掌握的是 structuredClone() 和手写递归。

JSON.parse(JSON.stringify())

这是最简单的一种深拷贝写法。

javascript 复制代码
const obj = {
  name: "Tom",
  info: {
    age: 18
  }
}

const clone = JSON.parse(JSON.stringify(obj))

这里涉及两个方法:

复制代码
JSON.stringify(obj)

先将对象转换成 JSON 字符串。

复制代码
JSON.parse(...)

再把 JSON 字符串解析成新的 JavaScript 对象。

因为重新创建了对象,所以嵌套对象不再与原对象共享引用。

但这种方式存在明显局限。

例如:

javascript 复制代码
const obj = {
  name: "Tom",
  age: undefined,
  sayHi() {},
  date: new Date(),
  reg: /abc/g
}

const clone = JSON.parse(JSON.stringify(obj))

执行之后:

  • undefined 属性会丢失。

  • 函数属性会丢失。

  • Date 会变成字符串。

  • RegExp 会变成普通空对象。

  • Map、Set 默认不能正确还原。

  • 遇到循环引用会报错。

  • 包含 BigInt 时通常会抛出异常。

因此:

JSON 方法适合结构简单、能够安全序列化为 JSON 的数据,不适合复杂对象的通用深拷贝。


structuredClone():现代 JavaScript 原生方法

现代浏览器和 Node.js 提供了:

复制代码
structuredClone()

可以直接实现多种数据类型的深拷贝。

例如:

javascript 复制代码
const obj = {
  name: "Tom",
  info: { age: 18 },
  date: new Date(),
  reg: /abc/g,
  map: new Map([["a", 1]]),
  set: new Set([1, 2, 3])
}

const clone = structuredClone(obj)

它能够处理普通对象、数组、Date、RegExp、Map、Set,以及循环引用等情况。

例如:

javascript 复制代码
const obj = {
  name: "Tom"
}

obj.self = obj

const clone = structuredClone(obj)

console.log(clone.self === clone)
// true

这里 obj.self 又指向 obj 自己,这种情况就叫循环引用。

structuredClone() 能正确保留这种引用关系。

但是它也有局限,例如不能克隆函数:

javascript 复制代码
const obj = {
  sayHi() {}
}

structuredClone(obj)
// DataCloneError

同时,它也不会完整保留普通自定义类实例的原型、所有属性描述符等信息。

因此在现代项目中,如果数据类型受支持,通常可以优先考虑 structuredClone()。


Lodash 的 cloneDeep()

Lodash 提供了一个深拷贝方法:

复制代码
_.cloneDeep()

例如:

javascript 复制代码
import cloneDeep from "lodash/cloneDeep"

const obj = {
  name: "Tom",
  info: {
    age: 18
  }
}

const clone = cloneDeep(obj)

它能够递归复制多种常见对象类型,也支持循环引用。

相比手写递归,cloneDeep() 使用更方便,处理情况也更加丰富。

不过它需要引入第三方库,而且也不是所有 JavaScript 对象都能被完全复制。


手写递归实现深拷贝

手写深拷贝是面试中最常考的部分。

最核心的思想是:

如果是基本类型,直接返回;如果是对象或数组,就创建新容器,并递归复制其中的内容。

先看最基础的版本:

javascript 复制代码
function deepClone(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj
  }

  const clone = Array.isArray(obj) ? [] : {}

  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key])
  }

  return clone
}

这里最重要的是:

javascript 复制代码
if (obj === null || typeof obj !== "object") {
  return obj
}

因为数字、字符串等基本类型不需要继续递归。

而:

复制代码
Array.isArray(obj) ? [] : {}

用于判断应该创建数组还是普通对象。

例如:

复制代码
const obj = {
  a: 1,
  b: {
    c: 2
  }
}

递归过程可以理解为:

复制代码
deepClone(obj)
    ↓
创建新对象
    ↓
复制 a → 1
    ↓
发现 b 还是对象
    ↓
再次调用 deepClone(b)
    ↓
创建新的 b 对象
    ↓
复制 c → 2
    ↓
返回完整的新对象

但是,这个基础版本只能处理普通对象和数组,遇到循环引用会无限递归,最终可能导致调用栈溢出。

所以还需要进一步优化。


为什么需要 WeakMap?

假设:

复制代码
const obj = {
  name: "Tom"
}

obj.self = obj

此时:

复制代码
obj.self === obj
// true

说明 obj 的 self 属性又引用了自己。

如果直接递归:

复制代码
复制 obj
   ↓
发现 self 是对象
   ↓
复制 self
   ↓
又发现 self 是对象
   ↓
继续复制......

就会陷入无限递归。

解决方法是使用 WeakMap,记录已经克隆过的对象。

复制代码
const hash = new WeakMap()

WeakMap 保存的是:

复制代码
原对象 → 克隆对象

核心代码:

复制代码
if (hash.has(obj)) {
  return hash.get(obj)
}

意思是:

如果这个对象之前已经克隆过,就直接返回之前创建的新对象,不再重复递归。

而:

复制代码
hash.set(obj, clone)

就是把原对象与新对象之间的关系保存起来。

使用 WeakMap 的另一个特点是,它的键必须是对象,且不会因为键被 WeakMap 引用而阻止该对象被垃圾回收。


完整递归实现

下面是在基础版本上加入循环引用处理,并兼容常见内置类型的面试实现:

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  // 基本类型和 null
  if (obj === null || typeof obj !== "object") {
    return obj
  }

  // 已经克隆过,直接返回
  if (hash.has(obj)) {
    return hash.get(obj)
  }

  // Date
  if (obj instanceof Date) {
    const clone = new Date(obj.getTime())
    hash.set(obj, clone)
    return clone
  }

  // RegExp
  if (obj instanceof RegExp) {
    const clone = new RegExp(obj.source, obj.flags)
    clone.lastIndex = obj.lastIndex
    hash.set(obj, clone)
    return clone
  }

  // Map
  if (obj instanceof Map) {
    const clone = new Map()
    hash.set(obj, clone)

    obj.forEach((value, key) => {
      clone.set(
        deepClone(key, hash),
        deepClone(value, hash)
      )
    })

    return clone
  }

  // Set
  if (obj instanceof Set) {
    const clone = new Set()
    hash.set(obj, clone)

    obj.forEach(value => {
      clone.add(deepClone(value, hash))
    })

    return clone
  }

  // 数组或普通对象
  const clone = Array.isArray(obj)
    ? []
    : Object.create(Object.getPrototypeOf(obj))

  hash.set(obj, clone)

  for (const key of Reflect.ownKeys(obj)) {
    clone[key] = deepClone(obj[key], hash)
  }

  return clone
}

这段代码相比基础版本,增加了几个关键处理。

WeakMap****解决循环引用:

javascript 复制代码
if (hash.has(obj)) {
  return hash.get(obj)
}

已经克隆过的对象直接复用,避免重复递归,并保留共享引用关系。

instanceof****判断特殊类型:

复制代码
obj instanceof Date
obj instanceof RegExp
obj instanceof Map
obj instanceof Set

不同类型使用对应的构造方式创建新实例,避免把它们当作普通对象处理。

**Object.create()**保留原型关系:

复制代码
Object.create(Object.getPrototypeOf(obj))

这里:

复制代码
Object.getPrototypeOf(obj)

用于获取原对象的原型。

而:

复制代码
Object.create(...)

会创建一个新对象,并把指定对象作为它的原型。

因此对于普通对象,可以保留原来的原型链关系。

**Reflect.ownKeys()**获取自身属性键:

复制代码
Reflect.ownKeys(obj)

可以获取对象自身的字符串属性键和 Symbol 属性键,包括不可枚举属性的键。

相比:

复制代码
Object.keys(obj)

它覆盖的属性键范围更广。

不过,这仍然是一个教学和面试实现,并不是完全通用的克隆工具。例如它没有完整保留 getter/setter、属性描述符,也没有专门处理 Error、TypedArray、DOM 节点、私有字段等特殊情况;函数仍然保留原引用。


测试深拷贝效果

javascript 复制代码
const obj = {
  name: "Tom",
  info: {
    age: 18
  },
  arr: [1, 2, 3],
  date: new Date(),
  reg: /abc/g,
  map: new Map([["a", 1]]),
  set: new Set([1, 2])
}

obj.self = obj

const clone = deepClone(obj)

console.log(clone === obj)
// false

console.log(clone.info === obj.info)
// false

console.log(clone.self === clone)
// true

这里可以看到,新对象和内部的 info 对象都被重新创建,而循环引用也得到了正确处理。

面试中应该怎么回答?

如果面试官问:

JavaScript 深拷贝有哪些实现方式?

可以这样回答:

JavaScript 深拷贝常见的实现方式有 JSON 序列化、structuredClone、Lodash 的 cloneDeep,以及手写递归。

JSON 方法实现简单,但不能正确处理函数、Date、RegExp、循环引用等特殊情况。

structuredClone 是原生深拷贝 API,支持循环引用和多种内置类型,但不支持函数等特殊对象。

手写递归时,需要先判断基本类型,再根据数组、普通对象、Map、Set 等类型分别处理,同时使用 WeakMap 记录已经克隆的对象,避免循环引用导致无限递归。

如果继续追问为什么使用 WeakMap,可以回答:

WeakMap 用来保存原对象和克隆对象的对应关系。递归时,如果发现对象已经克隆过,就直接返回之前的结果,从而避免循环引用和重复克隆。

总结

深拷贝的本质是:

重新创建对象,并递归复制嵌套结构,避免新旧对象共享需要复制的引用。

最重要的四种实现方式:

方法 适用场景
JSON 序列化 简单 JSON 数据
structuredClone 现代项目中的常见深拷贝需求
Lodash cloneDeep 项目已使用 Lodash、需要较广类型支持
递归 + WeakMap 手写算法、理解底层原理、定制需求

手写深拷贝最核心的逻辑可以记成:

复制代码
判断基本类型
      ↓
基本类型直接返回
      ↓
判断是否克隆过
      ↓
克隆过 → 返回缓存
      ↓
未克隆 → 创建新对象
      ↓
WeakMap 记录引用关系
      ↓
递归克隆内部属性
      ↓
返回新对象

一句话总结:浅拷贝只复制第一层引用关系,深拷贝递归复制嵌套对象,而 WeakMap 是处理循环引用和共享引用的关键。

相关推荐
@#¥&~是乱码鱼啦44 分钟前
ArkWeb开发手记01|Web 组件 H5 加载与生命周期管理
前端
suaizai_44 分钟前
ComputerUse:让AI真正操控桌面系统
java·前端·javascript
CodeSheep1 小时前
朋友面试谈薪报价2w,HR非得压到1.9,还反问他:你就差这1000块钱?后来背调时问了他前同事十几个问题,对方最后直接挂了
前端·后端·程序员
傲世仙尊1 小时前
C++起步-命名空间解决命名冲突与cin-cout缺省参数
开发语言·c++
hljqwb1 小时前
java服务异常日志只打印异常类型,没有堆栈定位分析(十)
java·开发语言
IMPYLH1 小时前
HTML 的 <track> 元素
前端·html
涛涛ing1 小时前
35个组件,把GPU特效直接跑在真实DOM上:Canvas UI正在重新定义Web视觉
前端
ym hyd 1111 小时前
生鲜超市库存管理信息系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
乘风gg1 小时前
Spec Kit vs OpenSpec vs Superpowers:8 个 Skill 的落地实录与工程方法论
前端·ai编程·claude