目录
概念
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的
深拷贝
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂)
浅拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址(新旧对象共享同一块内存),所以如果其中一个对象改变了这个地址,就会影响到另一个对象(只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃);
赋值和浅拷贝的区别
赋值
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝
会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
举例:
// 对象赋值
let obj1 = {
name: 'Chen',
age: 18,
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
let obj2 = obj1;
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
console.log('obj1===>', obj1);
console.log('obj2===>', obj2);
两者各属性都会进行改变
// 浅拷贝
let obj1 = {
name: 'Chen',
age: 18,
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
let obj3 = {...obj1};
obj3.name = 'Forever';
obj3.hobby[1] = 'swim';
obj3.hobby[2] = 'alpinism';
console.log('obj1===>', obj1);
console.log('obj3===>', obj3);
结合下表会有更深刻的理解。
实现
浅拷贝
注意:当拷贝对象只有一层的时候,是深拷贝
展开运算符…
let obj2 = {...obj1};
Object.assign()
let obj2 = Object.assign({}, obj1);
Array.prototype.concat()
let arr1 = [
{
name: 'Chen'
},
'see a film',
'write the code',
'play basketball',
'tourism'
];
let arr2 = arr1.concat([]);
Array.prototype.slice()
let arr2 = arr1.slice();
深拷贝
JSON.parse(JSON.stringify())
对某些数据不支持:
如Date类型会被转为字符串类型,
Undefined和RegExp类型丢失等问题。
无法拷贝存在循环引用的对象。
拷贝自身可枚举字符串属性,原型链丢失。
属性特性丢失。 性能较差。
原理
用JSON.stringify将对像转成JSON字符串,再用JSON.parse()把字符串解析成对象,新的对像产生了,而且对象会开辟新的栈
对象:
let obj2 = JSON.parse(JSON.stringify(obj1));
数组:
let arr2 = JSON.parse(JSON.stringify(arr1));
jQuery.extend()
需要引入jq库
let obj1 = jQuery.extend(true, {}, obj);
Lodash
const _ = require('lodash');
const obj1 = {
foo: 'bar',
arr: [1, 2, 3],
nestedObj: {
baz: 'qux'
}
};
const obj2 = _.cloneDeep(obj1); // 深拷贝对象
手写递归
原理:
遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
// 实现深拷贝(Object/Array)
const clone = (target) => {
let result;
let type = checkedType(target);
if(type === 'object') result = {};
else if(type === 'array') result = [];
else return target;
for (let key in target) {
if(checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
result[key] = clone(target[key]);
} else {
result[key] = target[key];
}
}
return result;
}
存在弊端——循环引用
什么是循环引用: 对象直接或间接地引用了自身
递归函数,看似已经解决了我们日常深拷贝的需要, 但是没有考虑到对象’循环引用’问题。
比如
obj.temp = obj; // obj中的属性temp的值指向了obj
const obj1 = clone(obj); // 无限循环下去 报错:栈内存溢出
obj中新增属性temp属性引用obj, obj中的temp中的temp属性引用了obj, 这就构成了循环引用;clone函数中, 循环调用clone,从而造成一个死循环导致爆栈
父级引用
就是上面的
obj.temp = obj; // obj中的属性temp的值指向了obj
同级引用
比父级往下一层
obj.detail['tempDetail'] = obj.detail; // obj.detail中的属性tempDetail的值指向了obj.detail
相互引用
你指我 我指你
obj.tempDetail= obj1;
obj1.tempDetail = obj;
优化——Map
针对深拷贝的循环引用问题,对clone函数进行优化:
开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
这个存储空间,需要可以存储key-value形式的数据,且key是一个引用类型。
我们可以选WeakMap这种数据结构:
检查WeakMap中有无克隆过的对象
有,直接返回
没有,将当前对象作为key,克隆对象作为value进行存储继续克隆
// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/\[object (\w+)\]/, "$1").toLowerCase();
// 实现深拷贝(Object/Array)
const clone = (target, hash = new WeakMap) => {
let result;
let type = checkedType(target);
if(type === 'object') result = {};
else if(type === 'array') result = [];
else return target;
if(hash.get(target)) return target;
let copyObj = new target.constructor();
hash.set(target, copyObj)
for (let key in target) {
if(checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
result[key] = clone(target[key], hash);
} else {
result[key] = target[key];
}
}
return result;
}
以上。
参考资料:
前端面试 第三篇 js之路 深拷贝与浅拷贝