深拷贝和浅拷贝的实现方法和区别
前言
了解深浅拷贝需要先了解两种数据类型,基本类型和引用类型。
基本类型
基本类型是简单的数据类型,它们存储的是值本身。在内存中,基本类型的值直接存储在变量的位置。JavaScript中的基本类型有:
-
Number(数字):整数或浮点数。
javascriptlet num = 42; // 数字 let pi = 3.14; // 浮点数
-
String(字符串):字符序列。
javascriptlet str = "Hello, World!"; // 字符串
-
Boolean(布尔值):表示真或假。
javascriptlet isTrue = true; // 真 let isFalse = false; // 假
-
Undefined(未定义):表示未初始化的变量。
javascriptlet undefinedVar;
-
Null(空值):表示没有值或空对象引用。
javascriptlet nullVar = null;
-
Symbol(符号):ES6引入的一种唯一标识符。
javascriptlet sym = Symbol('unique');
引用类型
引用类型是由多个值构成的对象,它们存储的是对象的引用(内存地址)。当操作引用类型时,实际上是在操作它们的引用而不是直接操作值。JavaScript中的引用类型包括:
-
Object(对象):包含键值对的集合。
javascriptlet person = { name: 'John', age: 30, };
-
Array(数组):包含有序元素列表的对象。
javascriptlet numbers = [1, 2, 3, 4, 5];
-
Function(函数):可执行的代码块。
javascriptfunction greet(name) { console.log(`Hello, ${name}!`); }
-
Date(日期):表示日期和时间的对象。
javascriptlet currentDate = new Date();
-
RegExp(正则表达式):用于匹配字符串的模式。
javascriptlet regex = /[a-z]/;
引用类型的值在内存中是通过引用存储的,因此对于相同的引用类型,它们可以共享相同的引用,即使它们在逻辑上是不同的对象。这就是为什么在进行浅拷贝时,只有引用被复制,而不是引用指向的对象的实际内容。深拷贝则是一种创建引用类型完全独立副本的方法。
1.浅拷贝
1.基本说明
浅拷贝是指创建一个新的对象或数组,复制源对象或数组的第一层元素到新对象或数组中。浅拷贝会复制基本类型的值直接到新对象,但对于引用类型(例如对象或数组),它只会复制它们的引用,而不会递归地复制它们的内部元素。简而言之,浅拷贝创建了一个新的对象或数组,但只复制了原始数据结构的表面层次,不会递归复制嵌套在原始结构中的对象或数组。
浅拷贝的特点是在创建副本时只复制原始对象或数组的第一层元素,而不会递归复制嵌套在其中的对象或数组。因此,如果你对浅拷贝的副本进行修改,这些修改可能会影响到原始对象或数组的第一层元素。但如果修改的是副本内的嵌套对象或数组,原始对象或数组不会受到影响。
javascript
// 原始对象
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.手动遍历复制对象属性
javascript
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. 扩展运算符(...)
javascript
const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = { ...ABC };
3. Object.assign()
javascript
const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = Object.assign({}, ABC);
4. 使用 Object.create()
javascript
const ABC = { key1: 'value1', key2: 'value2' };
const ABCCopy = Object.create(ABC);
数组:
5. Array.slice()
javascript
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()
javascript
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 中的一个静态方法,用于从一个类数组对象或可迭代对象创建一个新的数组实例。该方法接受两个参数:第一个参数是要转换成数组的对象,第二个参数是一个可选的映射函数,用于对数组的每个元素进行转换。
基本语法如下:
javascript
Array.from(arrayLike [, mapFunction [, thisArg]])
arrayLike
: 要转换成数组的对象或可迭代对象。mapFunction
(可选): 对数组中的每个元素执行的映射函数。thisArg
(可选): 映射函数中this
的值。
javascript
const ABC = [1, 2, 3, 4, 5];
const ABCCopy = Array.from(ABC);
字符串:
8. 使用 slice() 复制字符串
javascript
const ABC = "Hello, World!";
const ABCCopy = ABC.slice();
这些方法都可以用于创建原始对象或数组的浅拷贝,但需要注意的是,对于嵌套结构,这些方法只会复制嵌套对象或数组的引用,而不会创建它们的深层副本。如果需要深拷贝嵌套结构,需要考虑其他方法,例如手动递归遍历对象的属性。
2.深拷贝
1.基本说明
深拷贝是指在复制对象或数组时,不仅复制了原始对象或数组的第一层元素,还递归地复制了其内部所有层次的嵌套对象或数组,从而创建一个完全独立的副本。深拷贝确保了副本和原始对象之间的所有层次都是相互独立的,互不影响。
深拷贝通常通过递归遍历对象或数组的所有层次来实现,确保每个嵌套的对象或数组都被完全复制
对于深拷贝而言,副本将独立于原始对象,并且对副本的修改不会影响原始对象。深拷贝会递归复制对象的所有层,包括嵌套的对象或数组,以确保副本是完全独立的。
js
npm i lodash //安装依赖库
javascript
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. 递归手动实现
通过递归遍历对象或数组的所有层次,创建相应的副本。
javascript
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等特殊对象的情况。
javascript
const ABC = { key1: 'value1', key2: { abc: 'abcValue' } };
const deepCopyObj = JSON.parse(JSON.stringify(ABC));
3. 使用第三方库Lodash
许多第三方库提供了深拷贝的方法,其中最常用的是 Lodash 库的 _.cloneDeep()
方法。
javascript
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 的特点
- 双向通信:
MessageChannel
提供了两个MessagePort
对象,分别命名为port1
和port2
,它们都可以用于发送和接收消息。 - 消息传递: 通过调用
postMessage()
方法,可以在一个端口上发送消息,而通过在另一个端口上监听message
事件,可以接收消息。 - 传递通道:
MessageChannel
通常用于传递一次性或大块的数据,例如,可以使用Transferable
对象(例如,ArrayBuffer
)来传递大型数据结构,而无需复制数据。 - 跨上下文通信: 可以在主线程和 Web Worker、不同的窗口或 iframe 之间使用
MessageChannel
进行通信。
javascript
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.深浅拷贝主要区别
- 对象结构复制:
- 浅拷贝: 只复制对象的第一层属性,如果对象的属性值是对象,那么拷贝后的对象会引用相同的对象。
- 深拷贝: 复制整个对象结构,包括对象的所有嵌套属性,递归复制每个子对象,确保拷贝后的对象和原始对象是完全独立的。
- 引用关系:
- 浅拷贝: 对象的引用关系仅在第一层生效,即拷贝后的对象和原始对象的第一层属性是独立的,但如果属性值是对象,则两者之间共享相同的子对象。
- 深拷贝: 对象的引用关系在所有层级都被打破,确保拷贝后的对象和原始对象及其所有嵌套对象都是独立的,互不影响。
- 循环引用处理:
- 浅拷贝: 由于只复制第一层属性,对于包含循环引用的对象,浅拷贝可能陷入无限循环,导致栈溢出。
- 深拷贝: 通常需要额外的处理来解决循环引用问题,因为简单的递归复制可能导致无限递归。一些深拷贝实现会使用一些策略,例如记录已经拷贝过的对象,以避免重复拷贝。
- 性能:
- 浅拷贝: 通常比深拷贝更高效,因为它只复制对象的第一层属性,不需要递归整个对象结构。
- 深拷贝: 由于需要递归复制整个对象结构,深拷贝可能会消耗更多的时间和内存,尤其是在处理大型对象或对象树时。