前言
在日常使用中,其实我们也是经常的在使用着拷贝的相关概念,对于项目开发来说了解深浅拷贝的有关知识是很重要的。因此在面试中对于深浅拷贝的原理及实现,是会经常被问到的,下面本人将带大家深入的了解一下,深浅拷贝的概念及其实现方法。
浅拷贝
我们将要了解的是什么是浅拷贝,为什么需要浅拷贝,浅拷贝要怎么去实现?
什么是浅拷贝
浅拷贝是指创建一个新的对象,这个对象有着原始对象的一层属性的副本。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
那么我们为什么需要使用到浅拷贝呢
-
防止对象引用的副作用: 在 JavaScript 中,我们都知道对象是通过引用来传递的。如果我们直接将一个对象赋值给另一个变量,那么它们将指向同一个内存地址。这就意味着,如果修改其中一个对象,另一个对象也会受到影响。而浅拷贝可以创建一个新的对象,使得修改其中一个对象不会影响到另一个对象。
但是我们需要注意的是:浅拷贝只会复制对象的一层属性,而不会递归地复制所有嵌套对象的属性。这意味着,如果原始对象中包含了深层嵌套的对象或数组,浅拷贝无法完整地复制这些嵌套对象,而只是复制了它们的引用。因此,修改浅拷贝对象中的嵌套对象的属性会影响到原始对象。
-
数据独立性: 有时候我们需要对对象进行操作,但又不想影响到原始对象。这种情况下,浅拷贝可以为我们创建一个对象的副本,使得我们可以在副本上进行操作而不影响原始对象。
那么我们在日常中哪些方面会使用它呢?
- 对象复制: 当我们需要对一个对象进行复制,但又不想改变原始对象时,就会使用浅拷贝。例如,创建一个对象的备份以备份数据,或者创建一个对象的副本以进行修改而不影响原始对象。
- 传递参数: 在函数调用中,如果我们需要传递一个对象,并且希望在函数内部对该对象进行修改,但又不想修改原始对象,那么就可以使用浅拷贝来传递参数。这样可以保证原始对象的数据不会被改变。
实现方法
对于浅拷贝来说,我们有如下的实现方法:
- for in
- Object.assign()
- [...arr]
- slice()
- concat()
for in 遍历实现
js
Object.prototype.abc = 123
function shallowCopy(obj) {
let newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
let obj = {
a: [1,2,[3,4]],
b: {
c: 2,
d: 3
}
}
let obj2 = shallowCopy(obj);
obj.b.c = 4;
console.log(obj2);
如以上代码,我们可以通过for in
遍历源对象,通过hasOwnProperty
查找对象中是否有key,有的话就可以将obj
里面的属性赋给newObj
中,因为是赋值操作,我们可以看到,它将newObj的引用地址指向了obj的引用地址,因此实现了一个浅拷贝。
使用 Object.assign()
js
function shallowCopy(obj) {
return Object.assign({}, obj);
}
const obj = { name: '张三', age: 30 };
const newObj = shallowCopy(obj);
console.log(newObj); // { name: '张三', age: 30 }
因为Object.assign()
方法可以用于将一个或多个源对象的可枚举属性复制到目标对象,并返回目标对象。因此当目标对象是一个空对象时,相当于进行了浅拷贝。它会遍历源对象的可枚举属性,并将它们复制到目标对象中,但是只会复制对象的自身属性,不会复制原型链上的属性。
使用 [...arr]
js
function shallowCopy(arr) {
return [...arr];
}
const arr = [1, 2, 3];
const newArr = shallowCopy(arr);
console.log(newArr); // [1, 2, 3]
因为扩展运算符 ...
可以将一个数组展开为多个元素。因此当用于数组时,它会将数组中的每个元素分别取出,并放入一个新的数组中,从而实现了浅拷贝。但是这种方法适用于数组的浅拷贝,不能用于对象的拷贝。
使用 slice()
js
function shallowCopy(arr) {
return arr.slice();
}
const arr = [1, 2, 3];
const newArr = shallowCopy(arr);
console.log(newArr); // [1, 2, 3]
我们都知道slice()
方法会返回一个新的数组,包含从开始到结束(不包括结束,也就是左闭右开)选择的数组的浅拷贝部分。当不传入任何参数时,slice()
方法会复制整个数组,相当于进行了浅拷贝。但是该方法也只复制了数组的一层内容,不会递归复制嵌套数组或对象。
使用 concat()
js
function shallowCopy(arr) {
return arr.concat();
}
const arr = [1, 2, 3];
const newArr = shallowCopy(arr);
console.log(newArr); // [1, 2, 3]
concat()
方法可以用于合并两个或多个数组,并返回一个新的数组。因此当不传入任何参数时,concat()
方法会复制调用它的数组,相当于进行了浅拷贝。这种方法也只会复制数组的一层内容,不会递归复制嵌套数组或对象。
深拷贝
对于深拷贝,我们也要去了解什么是深拷贝,为什么需要深拷贝,深拷贝要怎么去实现?
什么是深拷贝
深拷贝是指在拷贝对象或数组时,将其所有层级的子对象和子数组都复制到新的对象或数组中,而不是简单地复制引用。换句话说,深拷贝会递归地复制对象及其所有子对象,从而完全独立于原始对象,修改深拷贝后的对象不会影响到原始对象。
那么我们为什么需要深拷贝呢?
主要有以下几个原因:
- 防止对象属性共享: 在 JavaScript 中,对象和数组都是引用类型,当我们对一个对象进行浅拷贝后,新对象和原对象共享同一份内存地址,修改新对象的属性会影响到原对象,这可能导致意外的副作用。深拷贝可以避免这种情况,确保新对象和原对象完全独立。
- 处理嵌套对象和数组: 当对象或数组中包含了嵌套的对象或数组时,浅拷贝无法完整地复制所有层级的子对象和子数组,而只是复制了它们的引用。这意味着,修改新对象或数组中的嵌套对象或数组会影响到原始对象或数组。深拷贝可以递归地复制所有层级的子对象和子数组,从而解决了这个问题。
- 保留对象的原型链关系: 深拷贝不仅会复制对象自身的属性,还会复制对象的原型链上的属性。这意味着,深拷贝后的对象与原始对象具有相同的原型链关系,可以保留对象的继承关系。
那么我们在日常中哪些方面会使用它呢?
深拷贝在日常开发中被广泛应用,特别是在处理复杂数据结构时更为常见。以下是一些常见的使用场景:
- 数据缓存与状态管理: 在前端开发中,经常需要对数据进行缓存或状态管理,深拷贝可以确保缓存的数据与原始数据完全独立,不会相互影响,从而保持数据的一致性和可靠性。
- 数据传递与组件通信: 在组件化开发中,经常需要将数据传递给子组件或通过事件总线进行组件间通信,深拷贝可以确保传递的数据在不同组件之间保持独立,避免意外修改导致的数据错误。
- 处理网络请求和异步操作: 在处理网络请求或异步操作返回的数据时,深拷贝可以确保操作的数据与原始数据完全独立,不会因为修改导致数据错乱或不一致。
- 处理复杂数据结构: 当数据结构较为复杂,包含嵌套对象或数组时,深拷贝可以递归地复制所有层级的子对象和子数组,确保数据的完整性和一致性。
实现方法
对于深拷贝,我们有如下实现方法:
- 递归实现
- JSON.parse(JSON.stringify(obj))
- structedClone()
- MessageChannel()
递归实现
递归实现是一种简单而常见的深拷贝方法。它通过递归地遍历对象的所有属性,并为每个属性创建一个新的对象或数组,从而实现深层次的复制。
js
function deepCopy(obj) {
let obj2 = {};
for (let i in obj) {
if (obj[i] instanceof Array) {
obj2[i] = [];
for (let j = 0; j < obj[i].length; j++) {
obj2[i][j] = obj[i][j];
}
}else if (typeof obj[i] === 'object') {
obj2[i] = deepCopy(obj[i]);
} else {
obj2[i] = obj[i];
}
}
return obj2;
}
let obj = {
a: [1,2,[3,4]],
b: {
c: 2
}
}
let obj2 = deepCopy(obj);
// obj.b.c = 4;
console.log(obj2);
从以上代码中我们可以看出,递归实现的深拷贝函数 deepCopy
遍历对象的所有属性,并对每个属性的值进行深拷贝。如果属性值是对象或数组,则递归地调用 deepCopy
函数来复制它们的子属性。这样就可以确保所有层级的子对象和子数组都被完整地复制,实现了深拷贝。
JSON.parse(JSON.stringify(obj))
js
let obj = {
a: 1,
b: {
c: 2
}
}
const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);
在上面的代码中,JSON.stringify(obj)
将对象序列化为 JSON 字符串,然后 JSON.parse()
将 JSON 字符串解析为新的对象,从而实现了深拷贝。
structuredClone()
structuredClone()
方法是 HTML Living Standard 中的一个新 API,它可以用于复制复杂的数据结构,包括对象、数组、Map、Set、Blob、File 等。这个方法主要用于 Web Worker 等环境中,在主线程和工作线程之间传递数据。对于 Web Worker
后面我们会做详细的介绍。
js
let obj = {
a: 1,
b: {
c: 2
}
}
const newObj = structuredClone(obj);
console.log(newObj);
structuredClone()
方法通过深度遍历对象及其所有的属性和子属性,并使用适当的复制算法来复制它们。这样就实现了完整的深拷贝。
MessageChannel()
MessageChannel()
是一个 HTML5 中的 API,它虽然主要用于在不同的窗口或 iframe 之间进行通信,但是也可以用于实现深拷贝。
js
let obj = {
a: 1,
b: {
c: 2
},
d:undefined,
}
function deepClone(obj) {
return new Promise((resolve) => {
const { port1, port2 } = new MessageChannel();
port1.postMessage(obj);
port2.onmessage = (msg) => {
resolve(msg.data);
}
})
}
let obj2 = null;
deepClone(obj).then((res) => {
console.log(res);
obj2 = res;
obj.b.c = 3;
console.log(obj2);
})
在上面的代码中,我们创建了一个 MessageChannel
,并从中获得了两个 MessagePort
对象 port1
和 port2
。 然后使用 port1.postMessage(obj)
将原始对象 obj
发送到 port1
。在 port2.onmessage
事件处理程序中,接收到了从 port1
发送过来的对象,并将其解析为新的对象,最后通过 resolve
返回新对象。在 deepClone
函数中使用了 Promise 包装整个过程,以便在异步操作完成后获取新对象。
因为 MessageChannel()
在不同线程之间传递数据时,会执行序列化和反序列化操作,所以即使原始对象发生改变,新对象不会受到影响,从而实现了深拷贝。
总结
浅拷贝
for in 遍历:
可以遍历对象的所有属性,并对每个属性进行复制。但是有着明显的缺点:只复制对象的一层属性,不会递归复制嵌套对象。如果原始对象包含嵌套对象或数组,浅拷贝无法完整地复制这些嵌套结构,而只是复制了它们的引用。因此,修改浅拷贝对象中的嵌套对象的属性会影响到原始对象。
Object.assign():
可以将一个或多个源对象的可枚举属性复制到目标对象。但是缺点是同样只复制对象的一层属性,不会递归复制嵌套对象。因此,对于包含嵌套结构的对象,使用 Object.assign()
也无法完整地复制所有层级的子对象。
扩展运算符 [...arr]
:
可以将一个数组展开为多个元素,并放入一个新的数组中。但是适用于数组的浅拷贝,但不能用于对象的拷贝。无法复制对象的属性,只能复制数组的内容。
slice()
方法:
可以返回一个新的数组,包含从开始到结束选择的数组的浅拷贝部分。虽然 slice()
方法可以复制整个数组,但它也只复制了数组的一层内容,不会递归复制嵌套数组或对象。
concat()
方法:
可以通过合并两个或多个数组,并返回一个新的数组来实现浅拷贝。但是与 slice()
方法类似,虽然 concat()
方法可以复制整个数组,但它也只复制了数组的一层内容,不会递归复制嵌套数组或对象。
深拷贝
递归实现:
可以通过递归地遍历对象的所有属性,并对每个属性进行深拷贝。但是实现较为复杂,需要递归地遍历对象的所有属性,并对每个属性进行深拷贝。对于包含循环引用或大型对象的情况,性能消耗较大,可能会导致栈溢出或内存泄漏。
JSON.parse(JSON.stringify(obj))
:
可以通过将对象序列化为 JSON 字符串,然后再解析为新的对象来实现深拷贝。此方法虽然可以简单地通过序列化和反序列化操作实现深拷贝,但该方法对于一些特殊的对象类型,如函数、正则表达式、Date 等,无法被完整地复制。此外,该方法也无法处理包含循环引用的对象,会导致堆栈溢出。
structuredClone()
:
可以通过深度遍历对象及其所有的属性和子属性,并使用适当的复制算法来复制它们以此实现深拷贝。虽然 structuredClone()
方法可以处理复杂的数据结构,并适用于大多数对象类型,但它在某些环境下可能不受支持(如旧版浏览器),并且在某些情况下可能存在性能问题。
MessageChannel()
:
可以在不同线程之间传递数据时,执行序列化和反序列化操作,实现完全独立于原始对象的深拷贝。虽然 MessageChannel()
可以在不同线程之间传递数据,并执行序列化和反序列化操作,从而实现深拷贝,但它仅适用于 Web Worker 等特定的环境,并且实现较为复杂,使用时需要额外的代码和处理逻辑。
综上所述,每种浅拷贝和深拷贝方法都有其特定的缺点,需要根据具体情况选择最合适的方法来进行对象或数组的拷贝。