如何在JavaScript中完美处理数据拷贝?你知道浅拷贝和深拷贝的区别吗?(超全分析)

1.浅拷贝

当我们谈论浅拷贝时,可以把它比作复印机上的"复制"按钮。想象一下你有一张纸,上面有一些图画和文字。你按下"复制"按钮,得到一份新的纸,这份新纸上的图画和文字与原始纸上的相同。

1.1 浅拷贝的基本概念

浅拷贝是指创建一个新的对象,将原始对象的属性值复制到新对象中。在这个过程中,对于基本数据类型,直接复制值;而对于引用数据类型,只复制引用,不深入复制引用指向的对象。因此,新对象和原始对象会共享引用类型的部分,修改一个对象的引用类型属性可能会影响另一个对象。

1.2 对基本数据类型的影响

在浅拷贝中,基本数据类型(如数字、字符串、布尔等)的处理较为简单,直接复制其值。这意味着新对象中的基本数据类型属性和原对象中的相应属性具有相同的值。这是因为基本数据类型是按值传递的,它们的值直接存储在变量中,而不是通过引用间接访问。

示例:

arduino 复制代码
const originalObject = { num: 42, str: "Hello", bool: true };
const shallowCopy = Object.assign({}, originalObject);

console.log(shallowCopy.num);  // 输出 42
console.log(shallowCopy.str);  // 输出 "Hello"
console.log(shallowCopy.bool); // 输出 true

在上述例子中,shallowCopy 中的属性值与 originalObject 中的相应属性值相同,因为这些属性都是基本数据类型。

1.3 对引用数据类型的影响

对于引用数据类型(如对象、数组)的处理就更加复杂了。浅拷贝只会复制引用,而不会递归复制引用指向的对象。这意味着新对象中的引用类型属性和原对象中的相应属性会指向同一个对象。如果修改其中一个对象的引用类型属性,另一个对象也会受到影响。

示例:

ini 复制代码
const originalObject = { array: [1, 2, 3], object: { key: 'value' } };
const shallowCopy = Object.assign({}, originalObject);

// 修改引用类型属性
shallowCopy.array.push(4);
shallowCopy.object.key = 'new value';

console.log(originalObject.array);  // 输出 [1, 2, 3, 4],影响原对象
console.log(originalObject.object.key);  // 输出 "new value",影响原对象

1.4 常见浅拷贝方法

1.4.1 Object.assign()

Object.assign() 方法可以用于浅拷贝对象。它将源对象的所有可枚举属性复制到目标对象,并返回目标对象。这个方法只会复制对象自身的属性,不会复制继承的属性。

ini 复制代码
const originalObject = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, originalObject);

// 修改引用类型属性
shallowCopy.b.c = 3;

console.log(originalObject.b.c);  // 输出 3,因为共享引用

1.4.2 扩展运算符 { ... }

扩展运算符 { ... } 可以用于对象和数组的浅拷贝。它将对象或数组中的元素展开到一个新对象或数组中。

ini 复制代码
const originalObject = { a: 1, b: { c: 2 } };
const shallowCopy = { ...originalObject };

// 修改引用类型属性
shallowCopy.b.c = 3;

console.log(originalObject.b.c);  // 输出 3,因为共享引用

1.4.3 Array.concat()

concat() 方法可以用于数组的浅拷贝。它将一个或多个数组合并成一个新数组。

ini 复制代码
const originalArray = [1, [2, 3]];
const shallowCopy = originalArray.concat();

// 修改引用类型属性
shallowCopy[1][0] = 4;

console.log(originalArray[1][0]);  // 输出 4,因为共享引用

1.4.4 Array.slice()

slice() 方法可以用于数组的浅拷贝。它返回一个新数组,包含从开始到结束(不包括结束)选择的数组的元素。

ini 复制代码
const originalArray = [1, [2, 3]];
const shallowCopy = originalArray.slice();

// 修改引用类型属性
shallowCopy[1][0] = 4;

console.log(originalArray[1][0]);  // 输出 4,因为共享引用

1.5 手动实现浅拷贝

手动实现浅拷贝意味着创建一个新对象,将原始对象的属性复制到新对象中。这里简单演示一个基本的手动浅拷贝方法:

ini 复制代码
function shallowCopy(original) {
  const copied = {};
  for (const key in original) {
    if (original.hasOwnProperty(key)) {
      copied[key] = original[key];
    }
  }
  return copied;
}

const originalObject = { a: 1, b: { c: 2 } };
const shallowCopyObject = shallowCopy(originalObject);

// 修改引用类型属性
shallowCopyObject.b.c = 3;

console.log(originalObject.b.c);  // 输出 3,因为共享引用

手动浅拷贝通过遍历原始对象的属性,将属性复制到新对象中。需要注意的是,这里使用 hasOwnProperty 来过滤掉原型链上的属性,确保只复制对象自身的属性。

2.深拷贝

2.1 深拷贝的基本概念

深拷贝是一种创建新对象的方式,它不仅复制原始对象的所有属性值,还递归地复制所有嵌套对象,确保新对象与原始对象完全独立。在深拷贝中,对于引用数据类型,不仅复制引用,还递归复制引用指向的对象,因此新对象和原始对象的引用类型部分是完全独立的。

2.2 对基本数据类型的影响

深拷贝对基本数据类型的处理方式与浅拷贝相同,直接复制其值。新对象中的基本数据类型属性与原对象中的相应属性具有相同的值。

2.3 对引用数据类型的影响

深拷贝的关键在于递归地复制引用类型的内容,确保新对象中的引用类型属性指向的是原对象中相应属性所指向的全新对象,而不是同一个对象的引用。这使得新对象和原对象的引用类型属性完全独立,互不影响。

示例:

ini 复制代码
const originalObject = { array: [1, 2, 3], object: { key: 'value' } };
const deepCopy = JSON.parse(JSON.stringify(originalObject));

// 修改引用类型属性
deepCopy.array.push(4);
deepCopy.object.key = 'new value';

console.log(originalObject.array);  // 输出 [1, 2, 3],不受影响
console.log(originalObject.object.key);  // 输出 "value",不受影响

在上述例子中,deepCopy 中的引用类型属性修改后,不会影响原对象中相应的属性。这是因为深拷贝递归地创建了一个新的对象结构。

2.4 常见深拷贝方法

2.4.1 JSON.parse(JSON.stringify())

利用 JSON.stringify() 将对象转换为 JSON 字符串,再利用 JSON.parse() 将 JSON 字符串转换为对象,从而实现深拷贝。但该方法无法处理包含函数、undefinedSymbol 等特殊类型的对象,并且会忽略属性值为 undefined 的属性。

ini 复制代码
const originalObject = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(originalObject));

// 修改引用类型属性
deepCopy.b.c = 3;

console.log(originalObject.b.c);  // 输出 2,不受影响

2.4.2 第三方库(例如 lodash 的 _.cloneDeep()

第三方库如 lodash 提供了深拷贝的专业实现,其中 _.cloneDeep() 方法可以安全地复制对象,处理了循环引用和特殊类型。

ini 复制代码
const _ = require('lodash');

const originalObject = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(originalObject);

// 修改引用类型属性
deepCopy.b.c = 3;

console.log(originalObject.b.c);  // 输出 2,不受影响

2.5 手动实现深拷贝

手动实现深拷贝需要递归地复制对象结构,确保每一层的对象都是全新的。以下是一个简单的手动深拷贝方法:

ini 复制代码
function deepCopy(original) {
  if (typeof original !== 'object' || original === null) {
    return original; // 如果是基本数据类型或 null,则直接返回
  }

  const copied = Array.isArray(original) ? [] : {}; // 判断是数组还是对象
  for (const key in original) {
    if (original.hasOwnProperty(key)) {
      copied[key] = deepCopy(original[key]); // 递归地复制每个属性
    }
  }
  return copied;
}

const originalObject = { a: 1, b: { c: 2 } };
const deepCopyObject = deepCopy(originalObject);

// 修改引用类型属性
deepCopyObject.b.c = 3;

console.log(originalObject.b.c);  // 输出 2,不受影响

这个手动深拷贝方法会递归地处理对象的每一层,确保每一层都是全新的对象。这样就避免了原始对象和深拷贝对象之间共享引用的问题。

3. 使用场景和注意事项

3.1 使用场景

3.1.1 浅拷贝的使用场景

  • 简单对象结构: 当对象结构比较简单,且不涉及嵌套或深层次的引用关系时,浅拷贝是一种轻量且有效的复制方式。
  • 性能要求较高: 浅拷贝通常比深拷贝更高效,适用于需要频繁复制对象且性能要求较高的场景。

3.1.2 深拷贝的使用场景

  • 嵌套或复杂对象结构: 当对象包含嵌套层次较深的结构,或者存在循环引用的情况时,深拷贝是确保独立性的选择。
  • 数据安全性: 在涉及到对对象进行修改、删除等操作时,深拷贝可以确保原始数据的安全性,不受外部修改的影响。
  • 对象共享不可行: 当需要独立的副本而不希望共享引用时,深拷贝是更为合适的选择。

3.2 注意事项

3.2.1 浅拷贝的注意事项

引用类型共享

浅拷贝只复制引用而不复制引用指向的具体内容,因此修改新对象中引用类型的属性可能会影响原对象。

ini 复制代码
const originalObject = { array: [1, 2, 3], object: { key: 'value' } };
const shallowCopy = Object.assign({}, originalObject);

// 修改引用类型属性
shallowCopy.array.push(4);
shallowCopy.object.key = 'new value';

console.log(originalObject.array);  // 输出 [1, 2, 3, 4],影响原对象
console.log(originalObject.object.key);  // 输出 "new value",影响原对象

在上述例子中,对 shallowCopy 进行的修改影响了原对象 originalObject 中相应的引用类型属性。

性能优势可能失效

虽然浅拷贝在性能上通常优于深拷贝,但在处理大型对象或数组时,由于共享引用,可能出现性能问题。

ini 复制代码
javascriptCopy code
const originalObject = { data: new Array(1000000).fill(0) };
const shallowCopy = Object.assign({}, originalObject);

// 修改引用类型属性
shallowCopy.data[0] = 1;

console.log(originalObject.data[0]);  // 输出 1,因为共享引用,性能优势失效

在上述例子中,尽管只修改了 shallowCopy 的属性,但由于共享引用,导致了性能优势的失效。

不适用于嵌套或复杂对象结构

浅拷贝通常适用于简单的对象结构,对于嵌套或包含复杂引用关系的对象,可能无法满足独立性的要求。

c 复制代码
const originalObject = { nested: { array: [1, 2, 3] } };
const shallowCopy = Object.assign({}, originalObject);

// 修改引用类型属性
shallowCopy.nested.array.push(4);

console.log(originalObject.nested.array);  // 输出 [1, 2, 3, 4],因为共享引用

在上述例子中,对 shallowCopy 的修改影响了原对象 originalObject 中嵌套属性的引用类型属性。

3.2.2 深拷贝的注意事项

性能开销较大

深拷贝需要递归地复制整个对象结构,这可能导致在处理大型复杂对象时的性能开销较大。每一层的递归都涉及到对象的创建和属性的复制,这在数据结构较为庞大时会显著影响性能。因此,在对性能要求较高的场景中,深拷贝的开销需要被慎重考虑。

不处理特殊对象

深拷贝可能无法正确处理一些特殊类型的对象,如包含函数、undefinedSymbol 、正则(变为空对象)等属性的对象。这是因为这些特殊类型的属性在 JSON 中无法被表示,因此在使用 JSON.parse(JSON.stringify(obj)) 进行深拷贝时,这些属性会被忽略或转化为 null

javascript 复制代码
const obj = { func: () => console.log('Hello'), undefinedProp: undefined, symbolProp: Symbol('symbol') };
const deepCopy = JSON.parse(JSON.stringify(obj));

console.log(deepCopy.func);  // 输出 null,函数被忽略
console.log(deepCopy.undefinedProp);  // 输出 null,undefined 被转化为 null
console.log(deepCopy.symbolProp);  // 输出 null,Symbol 被忽略
循环引用问题

深拷贝需要额外处理循环引用的情况,否则可能导致无限递归的问题。循环引用指的是对象属性之间形成了一个循环,导致深拷贝过程陷入无限递归。

ini 复制代码
const objA = { prop: null };
const objB = { prop: objA };
objA.prop = objB;

// 尝试深拷贝含有循环引用的对象
const deepCopy = JSON.parse(JSON.stringify(objA)); // 会抛出错误,因为存在循环引用

在处理含有循环引用的对象时,需要选择适当的深拷贝方法,例如使用专业的深拷贝工具或手动处理循环引用的情况。

4.结尾

希望这次关于浅拷贝和深拷贝的解释对你有所帮助。在编程中,了解如何正确地处理数据拷贝是非常重要的,因为它直接关系到数据的完整性和程序的稳定性。

记住,在选择拷贝方式时,需要根据具体情境考虑性能、独立性以及对特殊类型的处理。如果面对复杂的对象结构或需要确保数据安全性的场景,深拷贝可能是更好的选择。而在简单场景或对性能要求较高的情况下,浅拷贝可能更为合适。

如果你有任何关于拷贝、JavaScript或其他编程方面的问题,都可以随时提问。祝你编程愉快,探索无穷的代码世界!

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax