夏幻灵头像
关注
前端八股:JavaScript 深拷贝详解:实现方式、递归原理与循环引用封面图

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

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

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

理解深拷贝,首先需要知道 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) 会创建一个新对象,但只复制第一层属性。如果属性值是嵌套对象,复制的仍然是原对象的引用。

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) 会递归复制嵌套对象,使新旧对象之间不共享需要被克隆的嵌套引用。

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())

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

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

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

这里涉及两个方法:

JSON.stringify(obj)

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

JSON.parse(...)

再把 JSON 字符串解析成新的 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()

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

例如:

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,以及循环引用等情况。

例如:

const obj = {
  name: "Tom"
}

obj.self = obj

const clone = structuredClone(obj)

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

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

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

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

const obj = {
  sayHi() {}
}

structuredClone(obj)
// DataCloneError

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

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


Lodash 的 cloneDeep()

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

_.cloneDeep()

例如:

import cloneDeep from "lodash/cloneDeep"

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

const clone = cloneDeep(obj)

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

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

不过它需要引入第三方库,而且也不是所有 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
}

这里最重要的是:

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 引用而阻止该对象被垃圾回收。


完整递归实现

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

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 解决循环引用:

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 节点、私有字段等特殊情况;函数仍然保留原引用。


测试深拷贝效果

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 是处理循环引用和共享引用的关键。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/HX0942/article/details/167283658

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--