深拷贝和浅拷贝的实现方法和区别

深拷贝和浅拷贝的实现方法和区别

前言

了解深浅拷贝需要先了解两种数据类型,基本类型和引用类型。

基本类型

基本类型是简单的数据类型,它们存储的是值本身。在内存中,基本类型的值直接存储在变量的位置。JavaScript中的基本类型有:

  1. Number(数字):整数或浮点数。

    let num = 42; // 数字
    let pi = 3.14; // 浮点数
    
  2. String(字符串):字符序列。

    let str = "Hello, World!"; // 字符串
    
  3. Boolean(布尔值):表示真或假。

    let isTrue = true; // 真
    let isFalse = false; // 假
    
  4. Undefined(未定义):表示未初始化的变量。

    let undefinedVar;
    
  5. Null(空值):表示没有值或空对象引用。

    let nullVar = null;
    
  6. Symbol(符号):ES6引入的一种唯一标识符。

    let sym = Symbol('unique');
    

引用类型

引用类型是由多个值构成的对象,它们存储的是对象的引用(内存地址)。当操作引用类型时,实际上是在操作它们的引用而不是直接操作值。JavaScript中的引用类型包括:

  1. Object(对象):包含键值对的集合。

    let person = {
      name: 'John',
      age: 30,
    };
    
  2. Array(数组):包含有序元素列表的对象。

    let numbers = [1, 2, 3, 4, 5];
    
  3. Function(函数):可执行的代码块。

    function greet(name) {
      console.log(`Hello, ${name}!`);
    }
    
  4. Date(日期):表示日期和时间的对象。

    let currentDate = new Date();
    
  5. RegExp(正则表达式):用于匹配字符串的模式。

    let regex = /[a-z]/;
    

引用类型的值在内存中是通过引用存储的,因此对于相同的引用类型,它们可以共享相同的引用,即使它们在逻辑上是不同的对象。这就是为什么在进行浅拷贝时,只有引用被复制,而不是引用指向的对象的实际内容。深拷贝则是一种创建引用类型完全独立副本的方法。

1.浅拷贝

1.基本说明

浅拷贝是指创建一个新的对象或数组,复制源对象或数组的第一层元素到新对象或数组中。浅拷贝会复制基本类型的值直接到新对象,但对于引用类型(例如对象或数组),它只会复制它们的引用,而不会递归地复制它们的内部元素。简而言之,浅拷贝创建了一个新的对象或数组,但只复制了原始数据结构的表面层次,不会递归复制嵌套在原始结构中的对象或数组。

浅拷贝的特点是在创建副本时只复制原始对象或数组的第一层元素,而不会递归复制嵌套在其中的对象或数组。因此,如果你对浅拷贝的副本进行修改,这些修改可能会影响到原始对象或数组的第一层元素。但如果修改的是副本内的嵌套对象或数组,原始对象或数组不会受到影响。

// 原始对象
const ABC = {
    key1: 'value1',
    key2: 'value2',
    abc: {
      key3: 'value3',
      key4: 'value4'
    }
  };
  
  // 使用浅拷贝创建副本
  const ABCcopy = { ...ABC };
  
  // 修改浅拷贝的第一层元素
  ABCcopy.key1 = '第一层元素';
  
  // 修改浅拷贝的嵌套对象
  ABCcopy.abc.key3 = '嵌套对象';
  
  console.log(ABC);
//   {
//     key1: 'value1',
//     key2: 'value2',
//     abc: { key3: '嵌套对象', key4: 'value4' }
//   }
  console.log(ABCcopy);
//   {
//     key1: '第一层元素',
//     key2: 'value2',
//     abc: { key3: '嵌套对象', key4: 'value4' }
//   }

ABCcopy 是通过扩展运算符进行浅拷贝的。修改 ABCcopy 的第一层元素(key1)不会影响到原始对象,因为它们是基本类型值。然而,修改 ABCcopy 的嵌套对象(abc)的属性(key3)将会影响到原始对象的相应属性,因为它们是引用类型,浅拷贝只复制了引用。

所以,浅拷贝改变后,如果修改的是第一层元素,原对象或数组不受影响;但如果修改的是嵌套在其中的引用类型,原对象或数组可能会受到影响。这是因为浅拷贝只复制了引用,而不是引用指向的实际内容。

2.浅拷贝实现方法

普遍:
1.手动遍历复制对象属性
function CopyObject(obj) {
  const copy = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = obj[key];
    }
  }
  return copy;
}

const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = CopyObject(ABC);
对象:
2. 扩展运算符(…)
const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = { ...ABC };
3. Object.assign()
const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = Object.assign({}, ABC);
4. 使用 Object.create()
const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = Object.create(ABC);
数组:
5. Array.slice()
const ABC = [1, 2, 3, 4, 5];
const ABCCopy = ABC.slice(1,2);

console.log(ABC)  //[ 1, 2, 3, 4, 5 ]
console.log(ABCCopy) //[ 2 ]
6. Array.concat()
const ABC = [1, 2, 3, 4, 5];
const ABCCopy = [999,888,777].concat(ABC);

console.log(ABC)  //[1, 2, 3, 4, 5]
console.log(ABCCopy) //[999,888,777,1,2,3,4,5]
7. 使用 Array.from() 复制数组

Array.from() 是 JavaScript 中的一个静态方法,用于从一个类数组对象或可迭代对象创建一个新的数组实例。该方法接受两个参数:第一个参数是要转换成数组的对象,第二个参数是一个可选的映射函数,用于对数组的每个元素进行转换。

基本语法如下:

Array.from(arrayLike [, mapFunction [, thisArg]])
  • arrayLike: 要转换成数组的对象或可迭代对象。
  • mapFunction(可选): 对数组中的每个元素执行的映射函数。
  • thisArg(可选): 映射函数中 this 的值。
const ABC = [1, 2, 3, 4, 5];
const ABCCopy = Array.from(ABC);
字符串:
8. 使用 slice() 复制字符串
const ABC = "Hello, World!";
const ABCCopy = ABC.slice();

这些方法都可以用于创建原始对象或数组的浅拷贝,但需要注意的是,对于嵌套结构,这些方法只会复制嵌套对象或数组的引用,而不会创建它们的深层副本。如果需要深拷贝嵌套结构,需要考虑其他方法,例如手动递归遍历对象的属性。

2.深拷贝

1.基本说明

深拷贝是指在复制对象或数组时,不仅复制了原始对象或数组的第一层元素,还递归地复制了其内部所有层次的嵌套对象或数组,从而创建一个完全独立的副本。深拷贝确保了副本和原始对象之间的所有层次都是相互独立的,互不影响。

深拷贝通常通过递归遍历对象或数组的所有层次来实现,确保每个嵌套的对象或数组都被完全复制

对于深拷贝而言,副本将独立于原始对象,并且对副本的修改不会影响原始对象。深拷贝会递归复制对象的所有层,包括嵌套的对象或数组,以确保副本是完全独立的。

npm i lodash //安装依赖库
const _ = require('lodash')
const ABC = {
    key1: 'value1',
    key2: 'value2',
    abc: {
      key3: 'value3',
      key4: 'value4'
    }
  };
  
  // 使用深拷贝库,如Lodash中的_.cloneDeep()
  const deepCopy = _.cloneDeep(ABC);
  
  // 修改深拷贝的第一层元素
  deepCopy.key1 = '第一层元素';
  
  // 修改深拷贝的嵌套对象
  deepCopy.abc.key3 = '嵌套对象';
  
  console.log(ABC);
//   {
//     key1: 'value1',
//     key2: 'value2',
//     abc: { key3: 'value3', key4: 'value4' }
//   }
  console.log(deepCopy);
//   {
//     key1: '第一层元素',
//     key2: 'value2',
//     abc: { key3: '嵌套对象', key4: 'value4' }
//   }

使用 Lodash 库的 _.cloneDeep() 方法来执行深拷贝。无论修改的是第一层元素还是嵌套的对象,都不会影响到原始对象。这是因为深拷贝递归地创建了每个对象的副本,确保了所有嵌套结构的独立性。

深拷贝的特点是创建一个原始对象的完全独立副本,不受原始对象或副本之间修改的相互影响。这在需要保持数据完整性和避免副作用的情况下非常有用。

2.深拷贝实现方法

深拷贝的实现方法有很多,以下是一些常见的深拷贝方式:

1. 递归手动实现

通过递归遍历对象或数组的所有层次,创建相应的副本。

function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  let result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepCopy(obj[key]);
    }
  }

  return result;
}

const ABC = { key1: 'value1', key2: { abc: 'abcValue' } };
const deepCopyObj = deepCopy(ABC);
2. JSON 序列化与反序列化(JSON.parse和JSON.stringify)

JSON.parse() 用于解析 JSON 字符串,将其转换为相应的 JavaScript 对象。

JSON.stringify() 用于将 JavaScript 对象转换为 JSON 字符串。

通过将对象转换为JSON字符串,然后再将其解析回对象,实现深拷贝。这种方法有一些限制,例如无法处理包含函数、RegExp等特殊对象的情况。

ps:一些限制因素补充

  1. 特殊数据类型处理:JSON 格式不支持 undefined、function 和 Symbol 等特殊数据类型。因此,对包含这些类型的对象使用 JSON.stringify()JSON.parse() 会导致这些特殊类型被忽略或转换成其他类型,比如 undefined 被转换为 null。
  2. 循环引用处理:深拷贝在处理循环引用时会陷入无限递归,因为它试图无限制地复制对象,从而导致堆栈溢出或内存耗尽。JSON.parse(JSON.stringify()) 无法处理循环引用,因为 JSON 格式本身不支持循环引用。
const ABC = { key1: 'value1', key2: { abc: 'abcValue' } };
const deepCopyObj = JSON.parse(JSON.stringify(ABC));
3. 使用第三方库Lodash

许多第三方库提供了深拷贝的方法,其中最常用的是 Lodash 库的 _.cloneDeep() 方法。

const _ = require('lodash');
const ABC = { key1: 'value1', key2: { abc: 'abcValue' } };
const deepCopyObj = _.cloneDeep(ABC);
4. MessageChannel

在浏览器环境下,可以使用 MessageChannel 来创建对象的副本。

MessageChannel 是 HTML Living Standard 中定义的一种用于在不同上下文之间进行通信的 API。它主要用于在 Web 开发中实现跨文档、跨窗口、跨 iframe、跨文档对象模型 (DOM) 或者主线程和 Web Worker 之间进行异步消息传递。MessageChannel 创建了一个双向通信通道,通过两个相关联的 MessagePort 实例进行通信。

MessageChannel 的特点

  1. 双向通信: MessageChannel 提供了两个 MessagePort 对象,分别命名为 port1port2,它们都可以用于发送和接收消息。
  2. 消息传递: 通过调用 postMessage() 方法,可以在一个端口上发送消息,而通过在另一个端口上监听 message 事件,可以接收消息。
  3. 传递通道: MessageChannel 通常用于传递一次性或大块的数据,例如,可以使用 Transferable 对象(例如,ArrayBuffer)来传递大型数据结构,而无需复制数据。
  4. 跨上下文通信: 可以在主线程和 Web Worker、不同的窗口或 iframe 之间使用 MessageChannel 进行通信。
function deepCopyMessage(obj) {
  return new Promise(resolve => {
    const channel = new MessageChannel();
    channel.port1.onmessage = event => resolve(event.data);
    channel.port2.postMessage(obj);
  });
}

const ABC = { key1: 'value1', key2: { abc: 'abcValue' } };

deepCopyMessage(ABC).then(deepCopyObj => {
  console.log(deepCopyObj);
});

需要注意的是,并非所有对象都能被上述方法完美地深拷贝,例如包含循环引用、函数、RegExp等特殊对象的情况。在实际使用中,需要根据具体的需求选择最适合的深拷贝方式。

3.深浅拷贝主要区别

  1. 对象结构复制:
    • 浅拷贝: 只复制对象的第一层属性,如果对象的属性值是对象,那么拷贝后的对象会引用相同的对象。
    • 深拷贝: 复制整个对象结构,包括对象的所有嵌套属性,递归复制每个子对象,确保拷贝后的对象和原始对象是完全独立的。
  2. 引用关系:
    • 浅拷贝: 对象的引用关系仅在第一层生效,即拷贝后的对象和原始对象的第一层属性是独立的,但如果属性值是对象,则两者之间共享相同的子对象。
    • 深拷贝: 对象的引用关系在所有层级都被打破,确保拷贝后的对象和原始对象及其所有嵌套对象都是独立的,互不影响。
  3. 循环引用处理:
    • 浅拷贝: 由于只复制第一层属性,对于包含循环引用的对象,浅拷贝可能陷入无限循环,导致栈溢出。
    • 深拷贝: 通常需要额外的处理来解决循环引用问题,因为简单的递归复制可能导致无限递归。一些深拷贝实现会使用一些策略,例如记录已经拷贝过的对象,以避免重复拷贝。
  4. 性能:
    • 浅拷贝: 通常比深拷贝更高效,因为它只复制对象的第一层属性,不需要递归整个对象结构。
    • 深拷贝: 由于需要递归复制整个对象结构,深拷贝可能会消耗更多的时间和内存,尤其是在处理大型对象或对象树时。
  • 46
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狐说狐有理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值