【JavaScript】JavaScript中的深拷贝与浅拷贝详解:基础概念与区别

简介: JavaScript 中,理解数据拷贝的深浅至关重要。浅拷贝(如扩展运算符`...`、`Object.assign()`)仅复制对象第一层,共享内部引用,导致修改时产生意外联动。深拷贝(如自定义递归函数、`_.cloneDeep`或`JSON.parse(JSON.stringify())`)创建独立副本,确保数据隔离。选择哪种取决于性能、数据独立性和资源需求。深拷贝虽慢,但确保安全;浅拷贝快,但需小心引用共享。在面试中,理解这些概念及其应用场景是关键。

📚 引言

在JavaScript编程的奇妙之旅中,理解数据拷贝的机制是每位开发者绕不开的必修课。本文将带你深入浅出地探索深拷贝(Deep Copy)与浅拷贝(Shallow Copy)的概念、区别及其实现方法,让你的数据操作更加得心应手。🥳


❓什么是拷贝

「拷贝」,顾名思义,就是在计算机编程中创建一个已有对象或数据结构的副本的过程。根据拷贝的深度不同,可以分为 ==浅拷贝(Shallow Copy)== 和 ==深拷贝(Deep Copy)== 两种类型,它们在实际应用中扮演着不同的角色

Snipaste_2024-06-19_09-26-03.png

🌟 浅拷贝:表面功夫

「浅拷贝」仅涉及对象的第一层属性拷贝过程。当遇到嵌套的引用类型(比如对象、数组),浅拷贝并不会深入这些内部结构去创建新实例,而是直接复制这些内部对象的引用地址。这就意味着,尽管原对象与拷贝对象在表面上看似分离,实则在深层次结构上依旧紧密相连,彼此共享相同的数据稍有不慎的修改,就可能在二者之间产生意料之外的联动效果

实际作用

  1. 性能考虑:由于只复制一层,浅拷贝相对较快占用内存较少,适合于大型对象或深度嵌套结构中不需要完全独立的情况。
  2. 快速创建类似对象:当需要一个对象作为模板快速生成相似对象,但部分属性可能根据需要修改时,浅拷贝可以保留可共享的部分,仅修改差异部分。
  3. 资源节约:在某些场景下,如果确实不需要或不允许完全独立的副本,浅拷贝可以避免不必要的内存消耗。

下面通过一段代码来直观展现浅拷贝的特点:

const obj1 = {
   
   
    name : "Alice",
    age : 18
}
const obj2 = obj1 // obj2现在指向与obj1相同的内存地址
console.log("obj1",obj1) // 输出 obj1 { name: 'Alice', age: 18 }
console.log("---修改obj2的值后----") 
obj2.name = "Bob" // 修改了obj2指向的对象的name属性,由于obj1和obj2指向同一对象,所以这也会影响到obj1
console.log("obj1", obj1); // 输出 obj1 { name: 'Bob', age: 18 } -- obj1的值也确实发生了变化,因为它们共享同一对象
console.log("obj2", obj2); // 输出 obj2 { name: 'Bob', age: 18 } -- obj2的值同样为修改后的结果

在JavaScript中,当你将一个对象赋值给另一个变量时,实际上是将该对象的引用(内存地址)传递给了新变量,而不是创建了一个全新的对象副本。所以在上述代码中,obj2 = obj1意味着obj2obj1都指向了同一个对象在内存中的位置。

注释解释:

  • 当执行obj2.name = "Bob"时,实际上是在改变obj1obj2共同指向的那个对象的name属性值。因此,当你随后打印obj1时,会发现它的name属性也变成了"Bob"。
  • 这种现象体现了JavaScript中对象作为引用类型的特性,即==变量存储的是对象的引用而非对象本身==。因此,修改通过引用传递的对象会影响到所有指向该对象的变量。

浅拷贝常用方法

在JavaScript中实现浅拷贝有几种常用的方法,以下是其中的一些:

1. 扩展运算符 (...)

const originalObj = {
   
    a: 1, b: {
   
    c: 2 } };
const shallowCopyObj = {
   
    ...originalObj };

2. Object.assign()

const originalObj = {
   
    a: 1, b: {
   
    c: 2 } };
const shallowCopyObj = Object.assign({
   
   }, originalObj);

3. 手动遍历对象属性

可以通过循环遍历对象的键并手动复制它们到新的对象中,这种方法也可以实现浅拷贝。

function shallowCopy(obj) {
   
   
    let copy = {
   
   };
    for (let key in obj) {
   
   
        if (obj.hasOwnProperty(key)) {
   
   
            copy[key] = obj[key];
        }
    }
    return copy;
}

const originalObj = {
   
    a: 1, b: {
   
    c: 2 } };
const shallowCopyObj = shallowCopy(originalObj);

请注意,以上所有方法都只能实现对象第一层级的浅拷贝,对于嵌套的对象或数组,它们内部的引用类型数据仍然是共享的。如果需要深拷贝(即完全复制包括嵌套结构在内的所有数据),则需要使用其他技术,如递归实现的深拷贝函数或者使用JSON.parse(JSON.stringify())方法(但这种方法有局限性,比如不能处理函数和循环引用)。


🌠 深拷贝:彻底分离

「深拷贝」致力于打造完全独立的数据,与浅拷贝形成鲜明对比。它通过递归遍历的方式,一丝不苟地复刻对象的每一层级,直至触及那些不可分割的基本类型(字符串、数字、布尔值等),为每一级都锻造出崭新的镜像。这种做法确保了无论原对象如何变迁,拷贝出的新对象都能傲然独立,不受丝毫干扰,实现了数据的彻底隔离与保护。💪

实际作用

  1. 数据隔离:在需要确保数据独立性,避免操作拷贝对象时影响原对象的场景下,深拷贝非常关键,例如在多线程编程或并行处理中。
  2. 安全复制敏感数据:在复制包含敏感信息的对象时,深拷贝可以确保源数据的安全,因为修改拷贝不影响源数据。
  3. 持久化存储:当需要将对象序列化后保存或通过网络传输时,通常会使用深拷贝来创建一个完整的、独立的副本,以保证数据的完整性

下面的示例展示了如何手工实现一个深拷贝函数,并通过一个简单的测试用例来验证其功能:

// 自定义深拷贝函数
function deepClone(obj) {
   
   
    // 基础情况判断:非对象或为null,直接返回
    if (typeof obj !== 'object' || obj === null) return obj;

    // 根据obj类型创建相应的新容器
    let clone = Array.isArray(obj) ? [] : {
   
   };

    // 遍历原对象的每个属性
    for (let key in obj) {
   
   
        // 确保不是原型链上的属性
        if (obj.hasOwnProperty(key)) {
   
   
            // 递归拷贝当前属性值(无论基本类型还是引用类型)
            clone[key] = deepClone(obj[key]);
        }
    }
    // 返回构建完毕的拷贝对象
    return clone;
}

// 示例
let original = {
   
    a: 1, b: {
   
    c: 2 } }; // 原始对象包含嵌套
let deepCopy = deepClone(original); // 执行深拷贝

// 修改原始对象中的嵌套对象属性
original.b.c = 3;

// 输出拷贝对象中对应属性,验证深拷贝效果
console.log(deepCopy.b.c); // 输出 2,显示了深拷贝的独立性

这段代码首先定义了deepClone函数,它能够递归地处理任何层级的嵌套结构,为每一步都生成全新的副本。通过对比修改前后deepCopy对象的属性值,我们明确观察到深拷贝带来的彻底分离效果:即使原对象发生改变,深拷贝出的对象依然维持着最初的状态,完美体现了深拷贝的核心价值。

深拷贝常用方法

在JavaScript的世界里,不必每次都亲力亲为地手动实现深拷贝,许多强大的库和内建方法能助你一臂之力,使代码更加简洁高效。

1. Lodash 的 _.cloneDeep

Lodash 是一个广泛使用的JavaScript实用库,提供了丰富的函数来简化日常开发。其中的_.cloneDeep函数就是专门用来执行深拷贝的。

安装 Lodash(如果项目中尚未安装):

npm install lodash

使用 _.cloneDeep 方法:

const _ = require('lodash');

let original = {
   
    a: 1, b: {
   
    c: 2 } };
let deepCopyByLodash = _.cloneDeep(original);

2. JSON 的序列化与反序列化方法

利用 JSON.stringify()JSON.parse() 进行深拷贝是一种快速简便的方法,适用于简单对象结构。但请注意,这种方法存在以下限制:

  • 函数丢失 🚫: JSON格式不支持函数,因此对象中的函数会被忽略。
  • 循环引用问题 🔁: 如果对象中存在循环引用,此方法会抛出错误。
  • 日期对象转换 ⏰: 日期对象会被转换为字符串,丢失其原始的日期类型。

示例代码:

let original = {
   
    a: 1, b: {
   
    c: 2 } };
let deepCopyByJSON = JSON.parse(JSON.stringify(original));

数据丢失情况:

const obj1 = {
   
   
    a:Symbol('a'),                    // Symbol 会丢失
    b:undefined,                     // undefined 会丢失
    c:new Date(),                     // Date 转换成字符串形式
    d: new RegExp(/d/),             // RegExp 转换成 {}
    e:NaN,                            // NaN 会变成null
    f: function () {
   
   },                // 函数 function 会丢失
    g: new Map([['a',123]]),         // Map 转换成 {}
    h: new Set([123]),              // Set 转换成 {}
}
const obj2 = JSON.parse(JSON.stringify(obj1))
console.log('obj1',obj1)
console.log('obj2',obj2)

Snipaste_2024-06-19_09-28-21.png

3. 小贴士

  • 在选择工具方法时,考虑你的具体需求。对于简单场景,JSON方法可能足够用;而对于复杂对象或需要保留函数的情况,Lodash的_.cloneDeep或其他专门的深拷贝库可能是更佳选择。
  • 虽然自动化工具提供了便利,了解其背后的工作原理及限制也同样重要,这能帮助你在遇到特殊情况时做出正确的决策。

总之,合理利用现有工具,可以让深拷贝这一任务变得轻松许多,同时也让代码更加专注在业务逻辑上。


📊 深拷贝与浅拷贝的抉择

在软件开发过程中,选择「深拷贝」还是「浅拷贝」,往往取决于特定的场景需求和权衡考虑:

  • 性能考量 🚦:
    深拷贝由于其递归复制所有层级的特性,确实会导致更高的CPU和内存使用。对于包含大量元素或深层次嵌套的对象,这可能会显著增加处理时间和空间开销。因此,在性能敏感的应用中,若非必要,可能倾向于避免频繁使用深拷贝。

  • 数据独立性 🔒:
    当应用程序逻辑要求两个对象之间必须保持完全独立,任何一方的修改都不应影响另一方时,深拷贝是必要的选择。这对于维护数据一致性、避免意外副作用至关重要,尤其是在并发操作或状态管理复杂的系统中。

  • 资源节省 💰:
    浅拷贝通过仅复制对象的引用而非实际值,能够快速高效地完成拷贝操作,特别适合于对象结构较为简单或在短期内不需要修改引用对象的场景。这种方式减少了内存占用,提升了程序效率,但需谨慎使用,以免因共享引用引发的意外修改。

在决定深拷贝与浅拷贝的策略时,开发者需要综合考虑当前操作的上下文环境、对象的复杂度、性能要求以及对数据隔离程度的需求。简而言之:

  • 若追求执行速度和资源优化,且能接受数据间的潜在相互影响,则倾向于浅拷贝。
  • 若强调数据的完整独立性和长期稳定性,哪怕牺牲部分性能,深拷贝则是更为安全可靠的选择。

正确权衡这些因素,将有助于编写既高效又健壮的代码。


💯 面试考点

面试题1:

问题: 什么是JavaScript中的浅拷贝?请给出一个示例说明浅拷贝可能导致的问题。

答案: 浅拷贝是指创建一个新对象,但这个新对象的属性如果是引用类型(如对象、数组等),则只会复制这些引用类型的地址,而不是创建它们的副本。这意味着原始对象和拷贝对象会共享这些引用类型的数据。如果修改拷贝对象中的引用类型属性,原始对象的相关属性也会受到影响。

示例:

let original = {
   
    name: "Alice", details: {
   
    age: 30 } };
let shallowCopy = Object.assign({
   
   }, original);

// 修改拷贝对象中的引用类型属性
shallowCopy.details.age = 35;

console.log(original.details.age); // 输出 35,说明原始对象被影响了

面试题2:

问题: 如何在JavaScript中实现一个简单的深拷贝?并讨论JSON.parse(JSON.stringify())方法作为深拷贝的局限性。

答案: 实现深拷贝的一种简单方法是使用JSON.parse()JSON.stringify(),但这有其局限性。例如,这种方法不能处理函数、RegExp、Date、undefined等特殊类型的对象,也不能处理循环引用的情况。

示例代码:

// 可行的方式
let original = {
   
    a: 1, b: {
   
    c: 2 } };
let deepCopyByJSON = JSON.parse(JSON.stringify(original));
console.log(original, deepCopyByJSON) // 输出一致 { a: 1, b: { c: 2 } }
// 无法处理的情况
const obj1 = {
   
   
    a:Symbol('a'),                    // Symbol 会丢失
    b:undefined,                     // undefined 会丢失
    c:new Date(),                     // Date 转换成字符串形式
    d: new RegExp(/d/),             // RegExp 转换成 {}
    e:NaN,                            // NaN 会变成null
    f: function () {
   
   },                // 函数 function 会丢失
    g: new Map([['a',123]]),         // Map 转换成 {}
    h: new Set([123]),              // Set 转换成 {}
}
const obj2 = JSON.parse(JSON.stringify(obj1))
console.log('obj1',obj1) // obj1 { a: Symbol(a), b: undefined, c: 2024-05-09T15:51:21.907Z, d: /d/, e: NaN, f: [Function: f], g: Map(1) { 'a' => 123 }, h: Set(1) { 123 }}
console.log('obj2',obj2) // obj2 { c: '2024-05-09T15:51:21.907Z', d: {}, e: null, g: {}, h: {} }
// 循环引用出错的情况
let obj1 = {
   
   
  name: "Object 1"
};
let obj2 = {
   
   
  name: "Object 2"
};

// 形成循环引用
obj1.reference = obj2;
obj2.reference = obj1;

// 尝试使用 JSON.stringify() 进行深拷贝
try {
   
   
  let jsonString = JSON.stringify(obj1);
  let clonedObj = JSON.parse(jsonString);
  console.log("深拷贝成功:", clonedObj);
} catch (error) {
   
   
  console.error("深拷贝失败,原因:", error.message); // 循环引用会导致报错
}

面试题3:

问题: 描述一个实际应用场景,解释为什么在该场景下深拷贝比浅拷贝更合适。

答案: 假设你正在开发一个在线文档编辑器应用,用户可以打开一个文档模板进行编辑。每个用户应该能够独立编辑自己的版本,而不会影响到模板或其他用户的文档。在这种情况下,当你从模板创建一个新的文档时,你需要对模板进行深拷贝。

场景分析:

  • 浅拷贝会导致所有基于同一模板创建的文档实际上共享相同的对象引用,因此一个用户的编辑会影响到其他用户的文档内容。
  • 深拷贝则为每个新文档创建完全独立的副本,包括所有嵌套的对象和数组,这样每个用户可以自由编辑而不影响到模板或其他用户,保证了数据的隔离性和一致性。

因此,在这个场景下,深拷贝是更合适的选择,因为它确保了每个用户的工作环境是独立且安全的。


🎯 总结

在JavaScript的广阔天地里,深拷贝与浅拷贝是构建高效且健壮应用程序的基石。通过本次详尽的探讨,我们不仅理解了它们的基本概念,还深入分析了各自的优缺点及应用场景:

  • 浅拷贝如同表面的镜像,迅速且直接,适用于节省资源或快速复制简单结构,但需警惕潜在的共享引用风险。
  • 深拷贝则是彻底的复刻,确保数据的完全独立,虽在性能上有所牺牲,却为复杂逻辑和长期数据隔离提供了保障。

掌握了手动实现深拷贝的递归方法,同时也领略了利用现成工具(如Lodash的_.cloneDeep或JSON的序列化/反序列化)的便捷,这为我们提供了灵活多样的解决方案。

记住,没有绝对的好坏,只有最适合场景的选择。明智地根据项目需求和性能考量来决定使用浅拷贝还是深拷贝,是每个开发者必备的技能。


目录
相关文章
|
13天前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念,它允许我们基于现有的类(或构造函数)创建新的类
【6月更文挑战第15天】JavaScript继承促进代码复用与扩展,创建类层次结构,但过深的继承链导致复杂性增加,紧密耦合增加维护成本,单继承限制灵活性,方法覆盖可能隐藏父类功能,且可能影响性能。设计时需谨慎权衡并考虑使用组合等替代方案。
32 7
|
3天前
|
JavaScript 前端开发
JavaScript函数核心概念:用于代码复用与管理。
【6月更文挑战第25天】JavaScript函数核心概念:用于代码复用与管理。示例包括定义(无参、有参、有返回值)与调用,参数按值传递。函数内修改参数不影响外部变量。
8 1
|
4天前
|
XML 前端开发 JavaScript
HTML、CSS、JavaScript有什么区别
HTML、CSS、JavaScript有什么区别
|
6天前
|
JavaScript 前端开发 异构计算
JS中重排和重绘的区别是什么?
JS中重排和重绘的区别是什么?
14 1
|
9天前
|
JavaScript 前端开发 API
ECMAScript vs JavaScript: 理解两者间的联系与区别
ECMAScript vs JavaScript: 理解两者间的联系与区别
|
11天前
|
JavaScript 前端开发 索引
JS中的substr()和substring()函数有什么区别
JS中的substr()和substring()函数有什么区别
|
6天前
|
JavaScript 前端开发
JS中浅拷贝和深拷贝的区别
JS中浅拷贝和深拷贝的区别
8 0
|
10天前
|
JavaScript 前端开发 开发者
【JavaScript】JavaScript中call、apply与bind的区别:进阶特性与应用场景
【JavaScript】JavaScript中call、apply与bind的区别:进阶特性与应用场景
13 0
|
10天前
|
JavaScript 前端开发 安全
JS 严格模式和正常模式的区别
JS 严格模式和正常模式的区别
|
16天前
|
JavaScript 前端开发
深入解析JavaScript中的面向对象编程,包括对象的基本概念、创建对象的方法、继承机制以及面向对象编程的优势
【6月更文挑战第12天】本文探讨JavaScript中的面向对象编程,解释了对象的基本概念,如属性和方法,以及基于原型的结构。介绍了创建对象的四种方法:字面量、构造函数、Object.create()和ES6的class关键字。还阐述了继承机制,包括原型链和ES6的class继承,并强调了面向对象编程的代码复用和模块化优势。
24 0