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

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

文章目录

  • 深拷贝和浅拷贝的实现方法和区别
    • 前言
      • 基本类型
      • 引用类型
    • 1.浅拷贝
      • 1.基本说明
      • 2.浅拷贝实现方法
        • 普遍:
        • 1.手动遍历复制对象属性
        • 对象:
        • 2. 扩展运算符(...)
        • 3. Object.assign()
        • 4. 使用 Object.create()
        • 数组:
        • 5. Array.slice()
        • 6. Array.concat()
        • 7. 使用 Array.from() 复制数组
        • 字符串:
        • 8. 使用 slice() 复制字符串
    • 2.深拷贝
      • 1.基本说明
      • 2.深拷贝实现方法
        • 1. 递归手动实现
        • 2. JSON 序列化与反序列化(JSON.parse和JSON.stringify)
        • 3. 使用第三方库Lodash
        • 4. MessageChannel
    • 3.深浅拷贝主要区别

前言

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

基本类型

基本类型是简单的数据类型,它们存储的是值本身。在内存中,基本类型的值直接存储在变量的位置。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等特殊对象的情况。

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. 性能:
    • 浅拷贝: 通常比深拷贝更高效,因为它只复制对象的第一层属性,不需要递归整个对象结构。
    • 深拷贝: 由于需要递归复制整个对象结构,深拷贝可能会消耗更多的时间和内存,尤其是在处理大型对象或对象树时。

你可能感兴趣的:(前端面试相关,前端,javascript,vue.js)