最近做了一个后台项目,遇到了一个有关深浅拷贝的问题,虽然是个简单的小问题,但是本着查漏补缺的原则,于是乎,我重新梳理了一下有关深浅拷贝的知识。
数据类型
基本数据类型:Number、String、Boolean、Null、Undefined、Symbol
引用数据类型:Object
什么是深浅拷贝
浅拷贝是指创建一个新对象,但是该新对象的元素仍然是原始对象的引用。也就是说,在浅拷贝中,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,新对象和原始对象共享相同的内存地址,当修改其中一个对象时,另一个对象也会受到影响。浅拷贝通常适用于简单的数据结构。
深拷贝是指创建一个新对象,并且该新对象的元素与原始对象的元素完全独立,不存在引用关系。也就是说,在深拷贝中,是从堆内存中开辟一个新的区域存放新对象,新对象和原始对象拥有不同的内存地址,彼此之间的修改互不影响。深拷贝通常适用于复杂的数据结构,例如嵌套的对象或多维数组。
浅拷贝和赋值的区别
在区分深浅拷贝之前,我们还得提一下浅拷贝和赋值的一些容易忽视的,细微的,但却重要的区别:
赋值:当我们把一个对象赋值给一个新的变量时,赋的是该对象在栈中的内存地址,而不是堆中的数据。也就是两个对象指向同一个内存空间,无论哪个对象发生改变,其实都是改变的储存空间的内容,因此两个对象都是联动的。
js
const obj1 = {
a: 1,
b: { c: 2 }
};
// 赋值操作
const obj2 = obj1;
console.log(obj2); // { a: 1, b: { c: 2 } }
obj2.a = 5;
obj2.b.c = 10;
console.log(obj1); // { a: 5, b: { c: 10 } }
console.log(obj2); // { a: 5, b: { c: 10 } }
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享一块内存,会相互影响。
js
const obj1 = {
a: 1,
b: { c: 2 }
};
// 使用浅拷贝创建新对象
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: { c: 2 } }
obj2.a = 5;
obj2.b.c = 10;
console.log(obj1); // { a: 1, b: { c: 10 } }
console.log(obj2)// { a: 5, b: { c: 10 } }
浅拷贝
- 浅拷贝只复制对象的第一层属性,而不会递归复制嵌套对象的属性。
- 当使用浅拷贝复制一个对象时,新对象中的基本数据类型属性会被复制,但引用数据类型属性则只会复制引用(即指向同一个内存地址)。
- 修改新对象的引用数据类型属性会影响原始对象的引用数据类型属性,因为它们指向同一个对象。
js
// 简单实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
在js中,有几个常见的方式可以实现浅拷贝:
1.扩展运算符(...
):
js
// 浅拷贝数组
const originalArray = [1, 2, 3];
const shallowCopyArray = [...originalArray];
// 浅拷贝对象
const originalObject = { a: 1, b: 2 };
const shallowCopyObject = { ...originalObject };
2.Object.assign()
方法: 使用 Object.assign()
方法可以将一个或多个源对象的属性复制到目标对象中,实现浅拷贝。
js
const source = { a: 1, b: 2 };
const shallowCopy = Object.assign({}, source);
3.Array.prototype.slice()
方法和concat()
方法: 对于数组,可以使用 slice()
方法来创建一个包含相同元素的新数组,还可以使用 concat()
方法创建一个新数组,并将原始数组的元素添加到新数组中实现浅拷贝。
js
const originalArray = [1, 2, 3];
// slice方法
const shallowCopyArray = originalArray.slice();
// concat方法
const shallowCopyArray = originalArray.concat();
深拷贝
- 深拷贝会递归地复制对象的所有属性,包括嵌套对象的属性,确保原始对象和新对象完全独立,互不影响。
- 在深拷贝过程中,会创建一个全新的对象,并将原始对象的所有属性以及嵌套对象的属性都复制到新对象中。
js
// 递归实现一个简易深拷贝
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
深拷贝常见的方式有:
1.如上例所见编写一个循环递归函数实现深拷贝。
2.JSON 序列化与反序列化实现深拷贝: 可以利用 JSON 对象的 JSON.stringify() 和 JSON.parse() 方法实现深拷贝,将对象转换为 JSON 字符串再转换回对象,但是这种方式存在弊端,会忽略undefined、symbol和函数等特殊类型的属性。
js
const originalObject = { a: 1, b: { c: 2 } };
const deepCopyObject = JSON.parse(JSON.stringify(originalObject));
3.lodash库中提供的 _.cloneDeep()
方法,该方法能够处理各种特殊情况并确保完整复制对象的所有属性。
注意点
当进行深拷贝和浅拷贝时,需要注意以下几个方面:
1.循环引用问题:
循环引用是指对象之间相互引用形成闭环的情况。在进行深拷贝时,如果源对象存在循环引用,可能导致递归无限循环,最终导致栈溢出。为了解决这个问题,可以使用 Map 或 Set 数据结构来记录已经拷贝过的对象,遇到循环引用时直接返回之前拷贝的对象。
js
const obj = { name: "Alice" };
obj.self = obj; // 创建循环引用
// 浅拷贝会陷入循环拷贝中
const shallowCopy = Object.assign({}, obj);
console.log(shallowCopy); // 由于循环引用,会导致堆栈溢出
// 深拷贝可以解决循环引用问题
function deepCopyWithCircular(obj, cache = new WeakMap()) {
if (cache.has(obj)) return cache.get(obj);
let result = Array.isArray(obj) ? [] : {};
cache.set(obj, result);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = typeof obj[key] === 'object' ? deepCopyWithCircular(obj[key], cache) : obj[key];
}
}
return result;
}
const deepCopyWithCircularObj = deepCopyWithCircular(obj);
console.log(deepCopyWithCircularObj); // 深拷贝成功避免了循环引用问题
2.特殊属性处理:
在对象中存在特殊属性(如函数、正则表达式等)时,在进行深拷贝时需要特别处理。对于函数属性,可以选择是否拷贝函数定义;而对于正则表达式等特殊属性,可能需要手动创建新的对象进行拷贝。
js
function deepCopyWithSpecialProperties(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj = Array.isArray(obj) ? [] : {};
Object.entries(obj).forEach(([key, value]) => {
if (typeof value === 'function') {
// 处理函数属性
newObj[key] = value;
} else if (value instanceof RegExp) {
// 处理正则表达式属性
newObj[key] = new RegExp(value);
} else {
newObj[key] = deepCopyWithSpecialProperties(value);
}
});
return newObj;
}
// 创建含有特殊属性的对象
const specialObj = {
name: "Alice",
regExp: /test/,
sayHello: function() {
console.log("Hello!");
}
};
// 深拷贝对象并处理特殊属性
const copiedObj = deepCopyWithSpecialProperties(specialObj);
console.log(copiedObj);
3.原型链处理:
在深拷贝过程中,需要确保拷贝的对象和原对象的原型链保持一致,以保证方法和属性能够正确继承。可以使用 Object.create()
方法或手动复制原型链来实现原型链的正确拷贝。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log("My name is " + this.name);
};
const person = new Person("Alice");
// 浅拷贝只复制对象的自身属性,不包括原型链上的属性和方法
const shallowCopy = Object.assign({}, person);
shallowCopy.sayName(); // 报错,原型链上的方法未被拷贝
// 深拷贝需要手动复制原型链上的属性和方法
function deepCopyWithPrototype(obj) {
const result = Object.create(Object.getPrototypeOf(obj));
const propNames = Object.getOwnPropertyNames(obj);
propNames.forEach((key) => {
const propDesc = Object.getOwnPropertyDescriptor(obj, key);
Object.defineProperty(result, key, propDesc);
});
return result;
}
const deepCopiedPerson = deepCopyWithPrototype(person);
deepCopiedPerson.sayName(); // 输出 "My name is Alice",原型链上的方法也被正确拷贝
4.性能优化:
深拷贝涉及递归遍历对象属性,对于大型对象或嵌套层次深的对象可能会带来性能开销。为了优化性能,可以考虑使用缓存来记录已经拷贝的对象,避免重复拷贝;另外,也可以考虑采用循环代替递归来减少函数调用开销。
深拷贝和浅拷贝都有各自适用的应用场景,下面是它们的一些常见应用场景:
浅拷贝的应用场景
- 对象属性扩展: 当需要在一个对象上新增属性时,可以使用浅拷贝来创建一个新的对象,在新对象上添加属性。
- 状态管理: 在状态管理中,比如
vue
中的状态管理,在Vuex
状态管理中,当需要对state
中的数据进行一些临时性的修改或操作时,可以使用浅拷贝来创建一个临时的状态副本。这样可以确保在对副本进行操作时不会影响到Vuex
中的原始状态。 - 数据共享: 需要对数据进行简单的传递或共享时,可以使用浅拷贝来创建数据副本
深拷贝的应用场景
- 数据持久化: 当需要将对象保存到数据库或文件中时,使用深拷贝可以确保保存的对象是完全独立的,不会受原始对象的更改而影响。
- 修改数据: 当在
Vue
组件中需要对复杂嵌套对象或数组进行修改时,比如表单数据等需要保持数据独立性的情况下,需要使用深拷贝来创建对象副本。这样可以确保修改副本不会影响到原始数据。
总的来说,浅拷贝适用于简单的数据结构和不需要对嵌套对象进行修改的情况,而深拷贝则适用于需要完全独立、可修改的新对象的复杂情况。