首页 > web前端 > js教程 > 正文

js 怎么深拷贝一个对象

月夜之吻
发布: 2025-08-22 12:30:03
原创
743人浏览过

json.parse(json.stringify(obj)) 不能深拷贝一切,它会丢失或转换函数、undefined、symbol、regexp、date等类型,且不支持循环引用;2. 实现真正深拷贝的推荐方法是使用 structuredclone(),它能处理大多数内置对象和循环引用,但不支持函数和dom节点;3. 当需更高灵活性或兼容旧环境时,可自定义递归深拷贝函数,通过 weakmap 处理循环引用,并手动处理 date、regexp 等特殊类型;4. 深拷贝与浅拷贝的本质区别在于是否递归复制所有嵌套引用,深拷贝确保新旧对象完全独立;5. 选择方法时应权衡数据复杂度、环境兼容性和性能需求,优先使用 structuredclone(),其次考虑 json 方法或自定义函数,必要时引入 lodash 等库的 clonedeep 方法以获得最佳兼容性和健壮性。

js 怎么深拷贝一个对象

在JavaScript里,深拷贝一个对象意味着你需要创建一个全新的对象,它不仅复制了原始对象的所有属性值,更重要的是,如果这些属性值本身是引用类型(比如另一个对象或数组),深拷贝会递归地复制它们,确保新旧对象之间没有任何共享的引用。这样一来,你修改新对象的任何部分,都不会影响到原始对象。这与浅拷贝形成鲜明对比,浅拷贝仅仅复制顶层属性,嵌套的引用类型依然指向同一个内存地址。

解决方案

要实现一个可靠的深拷贝,根据你的具体场景和目标环境,有几种主流且实用的方法。

一种是利用

JSON
登录后复制
对象的序列化和反序列化能力。这种方式简洁明了,但有其局限性:

function deepCloneByJson(obj) {
  try {
    return JSON.parse(JSON.stringify(obj));
  } catch (e) {
    console.error("JSON深拷贝失败,可能存在不可序列化的数据类型或循环引用:", e);
    // 根据实际情况,这里可以返回原始对象或抛出错误
    return obj;
  }
}

// 示例
const originalObj = {
  a: 1,
  b: { c: 2 },
  d: [3, { e: 4 }],
  f: new Date(), // 会被转换为字符串
  g: undefined, // 会丢失
  h: function() {}, // 会丢失
  i: Symbol('test') // 会丢失
};

const clonedObj = deepCloneByJson(originalObj);
console.log(clonedObj);
// { a: 1, b: { c: 2 }, d: [ 3, { e: 4 } ], f: '2023-10-27T08:00:00.000Z' }
// 可以看到f变成了字符串,g、h、i都丢失了
登录后复制

另一种,也是现代浏览器和Node.js环境中更推荐的方法,是使用

structuredClone()
登录后复制
API。它设计之初就是为了解决复杂数据类型的深拷贝问题,并且能很好地处理循环引用:

function deepCloneStructured(obj) {
  if (typeof structuredClone === 'function') {
    try {
      return structuredClone(obj);
    } catch (e) {
      console.error("structuredClone深拷贝失败,可能存在不可克隆的数据类型(如函数、DOM节点等):", e);
      return obj; // 失败时返回原始对象
    }
  } else {
    // 兼容性处理,如果不支持structuredClone,退而求其次使用JSON方法
    console.warn("当前环境不支持structuredClone,尝试使用JSON方法进行深拷贝,但可能存在限制。");
    return deepCloneByJson(obj);
  }
}

// 示例
const originalComplexObj = {
  name: 'Test',
  details: {
    id: 123,
    timestamp: new Date(),
    regex: /abc/g
  },
  list: [1, 2, { nested: 'item' }],
  func: () => console.log('hello'),
  circular: null
};
originalComplexObj.circular = originalComplexObj; // 制造一个循环引用

const clonedComplexObj = deepCloneStructured(originalComplexObj);
console.log(clonedComplexObj);
// 可以看到timestamp和regex都被正确克隆,且func会被忽略(因为不可克隆)
// 循环引用也会被正确处理
登录后复制

JSON.parse(JSON.stringify(obj))
登录后复制
真的能深拷贝一切吗?

说实话,不能。虽然它在很多场景下非常方便,尤其是处理那些只包含数字、字符串、布尔值、null、普通对象和数组的“纯粹”JSON数据时,简直是神器。一行代码搞定,性能也不错。但一旦你的对象里开始出现一些“不那么JSON”的东西,比如

Date
登录后复制
对象、
RegExp
登录后复制
对象、
undefined
登录后复制
NaN
登录后复制
Infinity
登录后复制
、函数、
Symbol
登录后复制
Map
登录后复制
Set
登录后复制
,甚至是 DOM 节点或者复杂的类实例,它就力不从心了。

举个例子,

Date
登录后复制
对象会被转换成ISO格式的字符串,而不是一个新的
Date
登录后复制
实例。
undefined
登录后复制
、函数和
Symbol
登录后复制
类型的属性会直接被忽略掉。
NaN
登录后复制
Infinity
登录后复制
会变成
null
登录后复制
。更要命的是,如果你的对象存在循环引用(就是对象A引用了对象B,对象B又引用了对象A),
JSON.stringify()
登录后复制
会直接抛出错误,导致整个拷贝过程失败。这就像你试图用一个筛子去装水,总有些东西会漏掉或者变形。所以,如果对数据类型有严格要求,或者对象结构比较复杂,就得小心使用这种方法了。

面对复杂数据类型,如何实现真正的深拷贝?

JSON.parse(JSON.stringify())
登录后复制
的局限性变得无法接受时,我们通常会转向更强大的工具。在现代JavaScript世界里,
structuredClone()
登录后复制
API无疑是首选。它被设计来处理那些通过
postMessage
登录后复制
传递的数据类型,这意味着它能深拷贝大多数内置对象类型,包括
Date
登录后复制
RegExp
登录后复制
Map
登录后复制
Set
登录后复制
ArrayBuffer
登录后复制
TypedArray
登录后复制
ImageData
登录后复制
Blob
登录后复制
File
登录后复制
等,而且最关键的是,它能优雅地处理循环引用,不会像
JSON.stringify
登录后复制
那样直接报错。这让它在处理复杂数据结构时显得非常“省心”。

然而,

structuredClone()
登录后复制
也有它的“脾气”。它不能克隆函数(因为函数是不可结构化克隆的),也不能克隆 DOM 节点、Error 对象以及一些特殊的主机对象。对于这些不可克隆的类型,它会直接抛出
DataCloneError
登录后复制

那么,如果连

structuredClone()
登录后复制
都无法满足,比如你需要克隆一个包含函数的特定类实例,或者你的运行环境不支持
structuredClone()
登录后复制
怎么办?这时候,自定义递归深拷贝函数就派上用场了。虽然实现起来会复杂一些,尤其要考虑周全各种数据类型和循环引用,但它提供了最大的灵活性。

一个基本的递归实现思路是:

  1. 判断当前值是否为原始类型或
    null
    登录后复制
    ,是则直接返回。
  2. 判断是否为对象或数组。
  3. 处理循环引用:维护一个
    Map
    登录后复制
    WeakMap
    登录后复制
    来存储已拷贝过的对象和它们对应的克隆,避免无限递归。
  4. 根据是对象还是数组,创建新的空对象或空数组。
  5. 遍历原始对象的属性,对每个属性值递归调用深拷贝函数。
function customDeepClone(obj, hash = new WeakMap()) {
  // 处理原始类型和null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理日期对象
  if (obj instanceof Date) {
    return new Date(obj);
  }

  // 处理正则表达式
  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags);
  }

  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 创建新对象或新数组
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone); // 在递归之前将克隆对象放入hash

  // 递归拷贝属性
  for (let key in obj) {
    // 确保只拷贝对象自身的属性,不包括原型链上的
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      clone[key] = customDeepClone(obj[key], hash);
    }
  }

  return clone;
}

// 示例:包含函数、循环引用、日期和正则
const originalCustom = {
  a: 1,
  b: { c: 2 },
  d: [3, { e: 4 }],
  f: new Date(),
  g: /test/gi,
  h: function() { console.log('hello'); },
  i: Symbol('id'), // Symbol类型会直接被拷贝,但其值是唯一的
  j: null
};
originalCustom.j = originalCustom; // 制造循环引用

const clonedCustom = customDeepClone(originalCustom);
console.log(clonedCustom);
// 可以看到日期和正则被正确克隆为新实例,函数也被拷贝了(虽然通常不推荐拷贝函数)
// 循环引用也被处理了
登录后复制

这种自定义方法提供了最大的控制力,但实现起来也更考验功力,尤其是要确保没有遗漏各种边缘情况和特殊类型。在实际项目中,如果不是特别定制化的需求,我个人更倾向于优先考虑

structuredClone()
登录后复制

闪念贝壳
闪念贝壳

闪念贝壳是一款AI 驱动的智能语音笔记,随时随地用语音记录你的每一个想法。

闪念贝壳 53
查看详情 闪念贝壳

深拷贝与浅拷贝的本质区别是什么?

深拷贝和浅拷贝,这两个概念是理解JavaScript中对象复制的关键。它们的本质区别在于:是否彻底断开新旧对象在内存中的所有引用关系。

浅拷贝,顾名思义,它只会复制对象或数组的顶层属性。这意味着,如果原始对象中的某个属性值是基本类型(如数字、字符串、布尔值),那么新对象会得到一个独立的副本。但如果属性值是引用类型(比如另一个对象或数组),那么新对象复制的仅仅是这个引用本身的地址,而不是引用指向的实际内容。结果就是,新旧对象会共享这个嵌套的引用类型。你修改新对象中这个嵌套引用类型的属性,原始对象也会受到影响,反之亦然。这就像你复制了一本书的目录,但书页本身还是同一本。

常见的浅拷贝方式有:

  • 展开运算符 (
    ...
    登录后复制
    )
    :用于对象或数组。
    const obj1 = { a: 1, b: { c: 2 } };
    const shallowCopy1 = { ...obj1 };
    shallowCopy1.b.c = 3; // obj1.b.c 也会变成 3
    登录后复制
  • Object.assign()
    登录后复制
    :用于对象合并。
    const obj2 = { a: 1, b: { c: 2 } };
    const shallowCopy2 = Object.assign({}, obj2);
    shallowCopy2.b.c = 4; // obj2.b.c 也会变成 4
    登录后复制
  • Array.prototype.slice()
    登录后复制
    Array.from()
    登录后复制
    :用于数组。
    const arr1 = [1, { a: 2 }];
    const shallowCopyArr1 = arr1.slice();
    shallowCopyArr1[1].a = 3; // arr1[1].a 也会变成 3
    登录后复制

深拷贝则完全不同。它会递归地复制原始对象的所有层级,包括所有嵌套的引用类型。每当遇到一个引用类型,它都会创建一个全新的实例,并把原始实例的内容复制过去。这样一来,新对象和原始对象在内存中是完全独立的,它们之间没有任何共享的引用。你对新对象的任何修改,都不会对原始对象产生副作用,反之亦然。这就像你把一本书的每一页都复印了一遍,现在你有了两本完全独立但内容相同的书。这种彻底的独立性,正是深拷贝的价值所在。

选择深拷贝方法时,性能和兼容性如何权衡?

在选择深拷贝方法时,我通常会先问自己几个问题:

  1. 数据复杂吗? 有没有
    Date
    登录后复制
    RegExp
    登录后复制
    Map
    登录后复制
    Set
    登录后复制
    、循环引用,或者更奇葩的比如
    Blob
    登录后复制
    File
    登录后复制
  2. 目标环境是什么? 是现代浏览器、Node.js,还是需要兼容老旧IE浏览器?
  3. 性能要求高吗? 是拷贝一个小型配置对象,还是一个包含成千上万个嵌套对象的巨型数据结构?

基于这些考量,我个人会这样权衡:

  • JSON.parse(JSON.stringify(obj))
    登录后复制

    • 优点: 简单、代码量少、兼容性极好(几乎所有JavaScript环境都支持)、对于只包含JSON安全类型的小型到中型对象,性能通常不错。
    • 缺点: 局限性大,会丢失或转换非JSON安全类型(前面提过)。对于包含大量数据或深层嵌套的对象,性能可能不如
      structuredClone
      登录后复制
    • 适用场景: 快速深拷贝简单对象,例如从服务器获取的纯数据,或者你明确知道对象里没有那些“麻烦”的数据类型。我经常用它来快速复制一些配置对象,如果我确定它们不会有函数或日期等。
  • structuredClone()
    登录后复制

    • 优点: 功能强大,能处理大多数内置数据类型,包括
      Date
      登录后复制
      RegExp
      登录后复制
      Map
      登录后复制
      Set
      登录后复制
      ArrayBuffer
      登录后复制
      等,还能优雅处理循环引用。性能通常比
      JSON.parse(JSON.stringify())
      登录后复制
      更好,尤其是在处理大型或复杂数据时。
    • 缺点: 兼容性相对较新(需要较新的浏览器环境或Node.js 17+),不能克隆函数、DOM节点、Error对象等。
    • 适用场景: 当你需要深拷贝复杂数据结构,且目标环境支持时,这是我的首选。它省去了编写复杂递归逻辑的麻烦,同时提供了很好的健壮性。
  • 自定义递归深拷贝函数:

    • 优点: 灵活性最高,可以根据具体需求定制拷贝逻辑,例如,你可以决定是否拷贝函数、如何处理特定的类实例,或者在拷贝过程中进行数据转换。兼容性由你代码决定,可以支持所有环境。
    • 缺点: 实现复杂,容易出错(特别是处理循环引用和各种边缘类型),维护成本高。性能可能不如内置方法,尤其是在没有经过优化的情况下。
    • 适用场景:
      structuredClone()
      登录后复制
      无法满足你的特殊需求(例如必须拷贝函数),或者你需要支持非常老的JavaScript环境时。我个人只有在“万不得已”或者有非常明确的定制需求时才会去手写这种函数。
  • 第三方库(如Lodash的

    _.cloneDeep()
    登录后复制
    ):

    • 优点: 功能全面,考虑了各种复杂的边缘情况和数据类型,包括函数、DOM节点等(尽管通常不会深度拷贝DOM节点,但会妥善处理)。经过高度优化,性能通常也很好。
    • 缺点: 引入额外依赖,增加了项目体积。
    • 适用场景: 如果你的项目已经使用了Lodash或其他类似工具库,或者你需要一个经过充分测试且功能强大的通用深拷贝方案,那么使用它们提供的
      cloneDeep
      登录后复制
      方法会是一个非常省心且可靠的选择。

总结一下,我的建议是:优先考虑

structuredClone()
登录后复制
,它兼顾了功能和性能,是现代Web开发的理想选择。如果环境不支持或有特定不可克隆类型的需求,再考虑自定义递归函数。而
JSON.parse(JSON.stringify())
登录后复制
更多是作为快速、简单的“万金油”,但要牢记它的局限性。

以上就是js 怎么深拷贝一个对象的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
热门推荐
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号