JavaScript深拷贝与浅拷贝的实现方式

深拷贝与浅拷贝的核心区别在于复制深度,浅拷贝仅复制第一层属性,引用类型共享内存,修改嵌套属性会影响原对象;深拷贝则递归复制所有层级,实现完全独立。常用浅拷贝方法包括Object.assign、扩展运算符、slice和concat;深拷贝可使用JSON.parse(JSON.stringify(obj))(但不支持函数、undefined、Symbol、循环引用等),或通过递归函数结合WeakMap处理循环引用,更优方案是使用structuredClone() API,支持Date、RegExp、Map、Set及循环引用,但无法拷贝函数和DOM节点。在前端开发中,深拷贝适用于需要数据独立的场景如表单编辑、撤销功能,而浅拷贝适用于顶层复制或性能敏感场景。手动实现深拷贝需注意循环引用、特殊对象类型处理、性能开销及原型链问题,生产环境推荐使用structuredClone()或lodash等成熟库以确保健壮性。

javascript深拷贝与浅拷贝的实现方式

在JavaScript中,深拷贝与浅拷贝是处理对象和数组时绕不开的概念,它们的核心区别在于复制的深度。简单来说,浅拷贝只复制对象或数组的第一层属性,如果属性值是引用类型(如另一个对象或数组),那么新旧对象会共享这个引用;而深拷贝则会递归地复制所有层级的属性,确保新旧对象之间完全独立,互不影响。理解这一点,能帮助我们避免许多在数据操作中遇到的“意外”副作用。

解决方案

要实现JavaScript中的深拷贝与浅拷贝,我们有多种方法,每种都有其适用场景和局限性。

浅拷贝的实现方式

浅拷贝相对直接,通常用于处理对象或数组的顶层属性,或者当你知道内部没有需要独立复制的引用类型时。

立即学习“Java免费学习笔记(深入)”;

Object.assign()这是最常见的对象浅拷贝方式之一。它会将一个或多个源对象的所有可枚举的自有属性复制到目标对象。

const originalObject = { a: 1, b: { c: 2 } };const shallowCopy = Object.assign({}, originalObject);console.log(shallowCopy); // { a: 1, b: { c: 2 } }shallowCopy.a = 10;shallowCopy.b.c = 20;console.log(originalObject.a); // 1console.log(originalObject.b.c); // 20 (b 是共享的引用)

这里可以看到,a 是基本类型,修改 shallowCopy.a 不影响 originalObject.a。但 b 是一个对象,修改 shallowCopy.b.c 却会影响 originalObject.b.c,因为它们指向同一个内存地址。

扩展运算符 (...)无论是对象还是数组,扩展运算符都能提供简洁的浅拷贝语法。对于对象:

const originalObject = { a: 1, b: { c: 2 } };const shallowCopy = { ...originalObject };// 行为与 Object.assign 相同shallowCopy.b.c = 30;console.log(originalObject.b.c); // 30

对于数组:

const originalArray = [1, { a: 2 }, 3];const shallowCopy = [...originalArray];shallowCopy[0] = 10;shallowCopy[1].a = 20;console.log(originalArray[0]); // 1console.log(originalArray[1].a); // 20

Array.prototype.slice()Array.prototype.concat()这两个方法常用于数组的浅拷贝。

const originalArray = [1, { a: 2 }, 3];const shallowCopySlice = originalArray.slice();const shallowCopyConcat = [].concat(originalArray);// 行为与扩展运算符对数组的浅拷贝相同shallowCopySlice[1].a = 20;console.log(originalArray[1].a); // 20

深拷贝的实现方式

深拷贝则复杂一些,尤其当数据结构变得复杂时。

JSON.parse(JSON.stringify(obj))这是一种非常流行且简洁的深拷贝方式,因为它利用了JSON的序列化和反序列化过程。

const originalObject = {  a: 1,  b: { c: 2 },  d: new Date(),  e: undefined,  f: function() {}};const deepCopy = JSON.parse(JSON.stringify(originalObject));console.log(deepCopy);// { a: 1, b: { c: 2 }, d: "2023-10-27T10:00:00.000Z" } (Date对象被转换为字符串)// 注意:e (undefined) 和 f (function) 会丢失deepCopy.b.c = 40;console.log(originalObject.b.c); // 2 (原始对象未受影响)

这种方法的优点是简单,但缺点也很明显:

无法拷贝函数、undefinedSymbol 类型。Date 对象会被转换为字符串。RegExp 对象会变成空对象。无法处理循环引用,会抛出错误。

递归实现这是最通用、最灵活的深拷贝方式,可以处理各种复杂情况,包括特定类型的对象和循环引用(需要额外处理)。

function deepClone(obj, hash = new WeakMap()) {  if (obj === null || typeof obj !== 'object') {    return obj;  }  // 处理循环引用  if (hash.has(obj)) {    return hash.get(obj);  }  let cloneObj;  if (obj instanceof Date) {    cloneObj = new Date(obj);  } else if (obj instanceof RegExp) {    cloneObj = new RegExp(obj);  } else if (Array.isArray(obj)) {    cloneObj = [];  } else {    cloneObj = {};  }  hash.set(obj, cloneObj); // 记录已拷贝的对象  for (let key in obj) {    if (Object.prototype.hasOwnProperty.call(obj, key)) {      cloneObj[key] = deepClone(obj[key], hash);    }  }  return cloneObj;}const originalObject = {  a: 1,  b: { c: 2 },  d: new Date(),  e: /abc/g,  f: function() { console.log('hello'); },  g: null,  h: undefined};originalObject.self = originalObject; // 制造一个循环引用const deepCopy = deepClone(originalObject);console.log(deepCopy);deepCopy.b.c = 50;console.log(originalObject.b.c); // 2 (原始对象未受影响)console.log(deepCopy.f); // undefined (函数依然未被拷贝,因为typeof function是'function'而非'object',在deepClone开头被跳过)console.log(deepCopy.self === deepCopy); // true (循环引用被正确处理)

这个递归函数能处理大部分基本类型、数组、普通对象、Date和RegExp,并且通过WeakMap解决了循环引用的问题。不过,对于FunctionSymbol等类型,它默认会跳过或复制引用。如果需要更复杂的类型处理,比如拷贝MapSet,或者深度克隆函数(这通常不常见,因为函数行为通常是共享的),则需要进一步扩展。

structuredClone() API (Web API)这是一个相对较新的Web API,提供了一种标准化的深拷贝机制,功能强大且安全。它能处理许多JSON.parse(JSON.stringify())无法处理的类型,包括DateRegExpMapSetArrayBuffer等,并且能处理循环引用。

const originalObject = {  a: 1,  b: { c: 2 },  d: new Date(),  e: /abc/g,  f: new Map([['key', 'value']]),  g: new Set([1, 2, 3])};// originalObject.self = originalObject; // structuredClone也能处理循环引用// 检查浏览器支持if (typeof structuredClone === 'function') {  const deepCopy = structuredClone(originalObject);  console.log(deepCopy);  console.log(deepCopy.d instanceof Date); // true  console.log(deepCopy.e instanceof RegExp); // true  console.log(deepCopy.f instanceof Map); // true  deepCopy.b.c = 60;  console.log(originalObject.b.c); // 2} else {  console.warn("当前环境不支持 structuredClone API,请使用其他深拷贝方法。");}

structuredClone() 是目前浏览器端深拷贝的最佳实践,但在Node.js环境中,需要Node.js v17+或通过globalThis.structuredClone访问。它的缺点是不能拷贝函数和DOM节点。

为什么理解拷贝机制对前端开发至关重要?

在前端开发中,数据流动和状态管理是核心。如果对深浅拷贝的理解不够透彻,很容易在不经意间引入难以调试的bug,尤其是在处理复杂的状态或组件间数据传递时。比如,你可能在某个组件中修改了一个从父组件接收到的对象属性,本意是只影响当前组件的临时状态,结果却意外地修改了父组件甚至全局的状态,导致数据混乱或视图更新异常。

想象一下React或Vue这类框架,它们通常推崇不可变数据(Immutable Data)的概念。这意味着每次对数据的修改都应该生成一个新的数据副本,而不是直接修改原始数据。这样做的优点是:

可预测性: 数据的变化路径清晰,更容易追踪和调试。性能优化: 框架可以通过比较新旧数据的引用地址来快速判断是否需要重新渲染组件,避免不必要的性能开销。如果每次都修改原始数据,引用地址不变,框架就无法感知到变化,可能导致视图不更新。并发安全: 在多线程或异步操作中,不可变数据可以避免竞态条件,简化逻辑。

因此,当你需要修改一个对象或数组,并且希望这种修改不影响原始数据时,深拷贝就成了你的救星。反之,如果你确实需要共享引用(例如,优化内存或希望修改一个地方能同步影响所有引用),那么浅拷贝就是合适的选择。这种选择并非凭空而来,而是基于对数据结构和业务逻辑的深刻理解。

何时应该优先选择深拷贝而不是浅拷贝?

决定使用深拷贝还是浅拷贝,关键在于你对“独立性”的需求。当你的操作需要完全独立的数据副本时,深拷贝是唯一的选择。

修改副本不影响原始数据: 这是最常见的场景。比如,用户编辑表单数据,你希望在用户点击“保存”之前,对数据的修改只是暂时的,不影响原始数据源。如果用户取消编辑,你可以直接丢弃副本,原始数据保持不变。复杂嵌套数据结构: 当你的对象或数组包含多层嵌套的对象或数组时,如果只进行浅拷贝,内部的引用类型依然会共享。一旦修改了深层属性,原始数据也会被改变,这往往不是你想要的结果。历史记录/撤销功能: 实现“撤销”或“重做”功能时,你需要保存每个操作前的完整状态。深拷贝可以确保每个历史状态都是独立的快照,互不干扰。组件间数据隔离: 在组件化开发中,如果一个子组件接收了一个复杂对象作为props,并且需要在内部修改这个对象(通常是不推荐的,但有时业务逻辑复杂可能需要),为了不影响父组件的状态,子组件应该先对props进行深拷贝。

浅拷贝则适用于以下情况:

只需要复制顶层属性: 如果你的对象或数组只包含基本类型值,或者你明确知道内部的引用类型不需要独立复制,那么浅拷贝更高效。性能敏感的场景: 深拷贝,尤其是递归实现,可能会有性能开销,特别是对于非常大的或深度嵌套的数据结构。如果业务逻辑允许,浅拷贝通常更快。明确需要共享引用: 比如,你可能有一个配置对象,多个模块都需要读取和修改它的某个深层属性,并且希望这些修改能相互同步。在这种情况下,浅拷贝甚至不拷贝,直接传递引用可能更符合需求。

选择深拷贝通常意味着你对数据的独立性有更高的要求,愿意为这份独立性付出一定的性能代价(尤其是在自定义递归实现时)。而structuredClone() API的出现,大大降低了这种代价,并提升了深拷贝的易用性和健壮性。

深拷贝的实现有哪些常见的陷阱或性能考量?

深拷贝并非没有代价,尤其是在手动实现时,一些细节处理不好就容易踩坑。

循环引用(Circular References)这是最经典的陷阱。如果一个对象引用了它自身,或者对象A引用了对象B,对象B又引用了对象A,形成一个闭环,那么简单的递归深拷贝会陷入无限循环,最终导致溢出。

解决方案: 使用WeakMapMap来存储已经拷贝过的对象和其对应的副本。在递归拷贝之前,先检查当前对象是否已经在Map中。如果存在,直接返回其对应的副本,避免重复拷贝和无限循环。上面提供的deepClone函数就包含了这种处理。

特殊对象类型处理不当JSON.parse(JSON.stringify(obj))的局限性前面已经提过,它会丢失函数、undefinedSymbol,以及将DateRegExp等特殊对象转换为普通类型或字符串。

解决方案: 在递归深拷贝中,需要针对这些特殊类型进行显式判断和处理。例如,对于Date对象,需要new Date(obj);对于RegExp,需要new RegExp(obj)。对于MapSetArrayBuffer等,也需要相应的构造函数或迭代器来创建新的实例。函数通常不进行深拷贝,因为它们的行为是共享的,但如果你真的需要,可能需要考虑使用evalnew Function,但这通常是危险且不推荐的。

性能开销深拷贝,尤其是对大型或深度嵌套的数据结构进行递归拷贝时,会消耗大量的CPU和内存资源。每次递归调用都会创建新的对象和数组,并遍历所有属性。

考量:是否真的需要深拷贝? 重新评估业务需求,看是否可以通过浅拷贝、结构共享(如Immutable.js库)或其他设计模式来避免昂贵的深拷贝操作。只拷贝必要部分: 如果一个大对象中只有少数几个属性需要深拷贝,可以考虑只对这些特定属性进行深拷贝,其他部分保持浅拷贝或直接引用。structuredClone()的优势: 在浏览器环境中,structuredClone()通常比自定义的JavaScript递归实现更高效,因为它可以在底层利用更优化的算法。优先考虑使用它。第三方库: 像Lodash的_.cloneDeep等成熟库,它们通常对性能和各种边缘情况(如循环引用、特殊类型)做了很好的优化。在生产环境中,使用这些经过验证的库通常比自己手写更安全、更高效。

原型链问题简单的深拷贝通常只拷贝对象自身的属性,而会忽略原型链上的属性。这在大多数情况下是期望的行为,因为原型链上的方法和属性通常是共享的。但如果你的应用依赖于原型链上的某些可变状态,那么这可能是一个需要注意的点。

在实际开发中,如果对性能有极高要求,或者数据结构极其复杂,我们会倾向于使用structuredClone()(如果环境支持),或者选择像lodash.clonedeep这样经过优化的第三方库。自己手写递归深拷贝虽然能加深理解,但在生产环境,往往需要更健壮、更全面的解决方案来应对各种边缘情况。

以上就是JavaScript深拷贝与浅拷贝的实现方式的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1522224.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月20日 14:51:59
下一篇 2025年12月20日 14:52:14

相关推荐

  • Nuxt应用中如何优雅地移除或跳过JSON数据中的空字符串

    本文旨在解决Nuxt应用在处理包含空字符串的JSON数据时可能遇到的错误。我们将探讨两种主要策略:一是在数据加载阶段通过JavaScript进行预处理,有效过滤或移除空值对象;二是在Nuxt组件渲染时,利用条件渲染指令(如v-if)动态跳过或处理包含空字符串的元素,从而确保应用的稳定性和界面的正确显…

    2025年12月20日
    000
  • 使用jQuery实现汉堡菜单下拉框的点击切换显示/隐藏

    本教程详细介绍了如何利用jQuery和JavaScript实现一个常见的UI交互:点击汉堡菜单按钮时,切换其关联下拉菜单的显示与隐藏状态。通过一个简洁的HTML结构和几行jQuery代码,您将学会如何高效地控制页面元素的可见性,从而优化用户体验。 一、理解交互需求 在网页设计中,汉堡菜单(Hambu…

    2025年12月20日
    000
  • 解决jQuery操作复选框后视觉更新不一致的问题:以模态框交互为例

    本文详细探讨了在使用jQuery通过模态框交互来控制复选框选中状态时,界面视觉更新可能不一致的问题。文章通过分析this上下文和元素引用,提供了一个基于Bootstrap模态框的健壮解决方案,确保复选框状态能正确地在用户界面上反映出来,并附带完整示例代码和最佳实践。 问题背景与剖析 在Web开发中,…

    2025年12月20日
    000
  • 如何实现一个符合Promise A+规范的JavaScript Promise库?

    答案:实现符合Promise A+规范的Promise库需核心处理状态机、then链式调用与resolvePromise解析逻辑,支持异步回调、错误捕获及循环引用检测,确保状态不可逆、then返回新Promise并正确处理值类型。 要实现一个符合 Promise A+ 规范 的 JavaScript…

    好文分享 2025年12月20日
    000
  • 解决jQuery操作模态框后复选框视觉状态不更新的问题

    本文探讨了在使用jQuery通过模态框交互来控制复选框选中状态时,复选框视觉更新不同步的问题。核心在于this上下文的误用和模态框库的选择。通过存储复选框引用、使用Bootstrap模态框并正确调用prop()方法,可以确保复选框的视觉状态与逻辑状态保持一致,从而实现预期功能。 问题背景与分析 在w…

    2025年12月20日 好文分享
    000
  • LINE Bot 多消息类型回复:文本与贴图的组合发送指南

    本文旨在解决 LINE Bot 开发中,通过 Messaging API 组合发送文本消息和贴图时遇到的 400 Bad Request 错误。核心问题在于对同一 replyToken 进行多次 replyMessage 调用,而正确的做法是利用 API 支持在单次调用中发送一个消息数组,从而实现文…

    2025年12月20日
    000
  • 在Apollo Server中集成Neo4j图数据并正确返回关联节点

    本文详细介绍了如何在Apollo Server中结合Neo4j数据库,通过GraphQL查询并正确映射和返回中心节点及其关联节点。我们将探讨GraphQL模式定义、Neo4j数据查询以及Apollo Server解析器(Resolver)的实现细节,特别是如何处理嵌套的关联节点数据,确保数据结构与G…

    2025年12月20日
    000
  • 如何通过 JavaScript 的 Performance Observer 监控长任务与卡顿?

    答案:通过PerformanceObserver结合Long Tasks API可监控执行超50ms的长任务,利用duration、startTime和attribution等数据定位卡顿源头,统计频率与耗时并节流上报,有效优化页面流畅度。 要监控网页中的长任务和卡顿,JavaScript 提供了 …

    2025年12月20日
    000
  • 深入理解JavaScript对象数组的动态属性排序

    本文深入探讨了一个JavaScript函数propSort,它利用Array.prototype.sort()方法,根据对象数组中指定数字属性的值进行排序。文章详细解释了sort()方法的工作原理、比较函数的逻辑,特别是如何通过字符串动态访问对象属性,以及如何处理缺失或空值。此外,还提供了TypeS…

    2025年12月20日
    000
  • JavaScript中的原型链继承与Class语法糖有何本质联系?

    JavaScript中Class是原型链继承的语法糖,本质仍基于构造函数和原型链机制,通过extends实现子类原型链接父类原型,与Object.create效果一致。 JavaScript中的原型链继承与Class语法糖本质上是同一套继承机制的不同表现形式。Class并不是一个全新的继承模型,而是…

    2025年12月20日
    000
  • Redux Toolkit中createSlice状态更新的常见陷阱与解决方案

    本文深入探讨了Redux Toolkit中createSlice状态管理的一个常见问题:当reducer函数返回原始值而非完整状态对象时,可能导致状态丢失或变为undefined。文章通过一个实际案例,详细解析了setAccuracy reducer的错误实现,并提供了两种正确的更新状态方式,强调了…

    2025年12月20日
    000
  • CSS Transition 仅在第二次点击时生效的解决方案

    本文旨在解决 CSS transition 在首次点击时无效,需要第二次点击才能生效的问题。通过分析问题代码,我们发现事件监听器被错误地放置在点击事件处理函数内部,导致监听器在第一次点击后才被绑定。本文将提供修改后的代码示例,确保 transition 效果在第一次点击时即可正常触发,并深入探讨事件…

    2025年12月20日
    000
  • 深入理解JavaScript中基于键合并数组对象的方法

    本文详细阐述了如何在JavaScript中,利用数组的reduce方法高效地将一个包含多种类型对象的数组,根据共享的键(key)进行合并,从而生成结构统一、数据完整的复合对象。教程将通过示例代码,逐步解析合并逻辑,帮助开发者掌握数据聚合与重构的关键技巧。 问题场景:异构数据合并 在数据处理中,我们经…

    2025年12月20日
    000
  • jQuery实现动态汉堡菜单:点击切换显示与隐藏

    本教程详细介绍了如何利用jQuery实现一个动态的汉堡菜单功能。通过绑定点击事件,菜单可以在点击按钮时平滑地切换显示与隐藏状态,确保用户界面简洁高效。文章提供了清晰的HTML结构、核心JavaScript代码及其解析,并强调了初始状态设置和jQuery库引入等关键注意事项。 在现代Web开发中,汉堡…

    2025年12月20日
    000
  • 响应式网页设计:解决浏览器窗口动态调整时横向滚动到纵向滚动的切换问题

    本文旨在解决响应式网页设计中,当浏览器窗口从宽屏模式动态调整到窄屏模式(例如1025px以下)时,网站滚动方向无法正确从横向切换到纵向的问题。我们将深入分析导致此问题的CSS媒体查询和JavaScript事件处理逻辑,并提供一套完整的解决方案,确保网站在不同视口宽度下均能实现流畅且符合预期的滚动行为…

    2025年12月20日
    000
  • JavaScript中的装饰器(Decorators)目前有哪些实用的应用?

    装饰器通过声明式语法为类和方法添加功能,广泛用于性能监控、权限控制、防抖节流及元数据配置,实现代码解耦与复用,在TypeScript和现代框架中已成为提升开发效率的实用工具。 JavaScript中的装饰器虽然仍是实验性特性,需要Babel或TypeScript等工具支持,但已在多个实际场景中展现出…

    2025年12月20日
    000
  • 如何用Node.js与Docker容器化一个应用?

    答案:将 Node.js 应用容器化需准备代码、编写 Dockerfile、构建镜像、运行容器。首先确保项目含 package.json 和入口文件,定义启动脚本;接着创建 Dockerfile,基于 node:18-alpine 镜像,设置工作目录,分步拷贝依赖并安装,暴露端口并设定启动命令;通过…

    2025年12月20日
    000
  • JavaScript中基于不同键路径合并复杂JSON数据

    本教程详细讲解如何在JavaScript中合并一个包含复杂JSON对象的数组。面对键(key)可能存在于顶层或嵌套结构(如confidential.key)中的情况,我们将演示如何利用Array.prototype.reduce方法高效地将具有相同键的所有相关信息合并成一个单一的对象,从而生成结构清…

    2025年12月20日
    000
  • CSS Transition 需要点击两次才能生效的解决方案

    本文旨在解决 CSS transition 在特定场景下需要点击两次才能生效的问题。通过分析问题代码,找出事件监听器重复绑定的原因,并提供修改后的代码示例,确保 transition 效果在第一次点击时就能正确触发。文章还将讨论如何避免类似问题的发生,以及如何优化 CSS transition 的性…

    2025年12月20日
    000
  • 如何构建一个响应式、自适应的数据表格组件?

    答案:构建响应式数据表格需结合语义化HTML、CSS弹性布局与JavaScript交互优化,通过data-label属性、媒体查询与堆叠布局适配多端,支持可访问性与虚拟滚动等性能优化。 构建一个响应式、自适应的数据表格组件,关键在于让表格在不同屏幕尺寸下都能清晰展示数据,同时保持良好的交互体验。核心…

    2025年12月20日
    000

发表回复

登录后才能评论
关注微信