JavaScript学习之路: 艰难且漫长, 摸爬滚打这些年,还剩头上三根毛,这是我学习的见证呀😎
开始我的学习之路~~🙆♀️
深拷贝与浅拷贝
想必你也有以下疑问:
- 什么是拷贝(copy) ❓
- 那深拷贝与浅拷贝什么 ❓
- 如何实现深拷贝与浅拷贝呢 ❓
- ...... ❓
接下来,就让我们一起去探索!
深拷贝和浅拷贝是只针对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);
上述例子,obj1是原数据,obj2是直接赋值得到的数据,obj3是通过浅拷贝得到的; 可清晰对比其对原数据的影响
对原始数据的影响 | |||
---|---|---|---|
-- | 和原数据是否指向同一对象 | 第一层数据未基本数据类型 | 原数据包含子对象(引用数据类型) |
赋值 | 是 | 赋值后的数据改变,会使原数据一同改变 | 赋值后的数据改变,会使原数据一同改变 |
浅拷贝 | 否 | 浅拷贝后的数据改变,不会使原数据一同改变 | 赋值后的数据改变,会使原数据一同改变 |
浅拷贝的实现 注意:当拷贝对象只有一层的时候,是深拷贝
- 展开运算符...
// 展开运算符... 实现浅拷贝
let obj1 = {
name: 'Chen',
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
let obj2 = {...obj1};
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
obj2.name = 'Forever';
console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: [ 'see a film', 'swim','alpinism', 'tourism']}
console.log('obj2===>', obj2); // obj2===> { name: 'Forever',hobby: [ 'see a film', 'swim','alpinism', 'tourism']}
- Object.assign()
// Object.assign() 实现浅拷贝
let obj1 = {
name: "Chen",
hobby: ["see a film", "write the code", "play basketball", "tourism"],
};
let obj2 = Object.assign({}, obj1);
obj2.hobby[1] = "swim";
obj2.hobby[2] = "alpinism";
obj2.name = "Forever";
console.log("obj1===>", obj1); // obj1===> {name: 'Chen',hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]}
console.log("obj2===>", obj2); // obj2===> {name: 'Forever',hobby: [ 'see a film', 'swim', 'alpinism', 'tourism' ]}
当object只有一层的时候,是深拷贝;所以当原数据进行浅拷贝,改变obj2的name 原数据obj1中的name不会改变;
- Array.prototype.concat()
// Array.prototype.concat() 实现浅拷贝
let arr1 = [
{
name: 'Chen'
},
'see a film',
'write the code',
'play basketball',
'tourism'
];
let arr2 = arr1.concat([]);
arr2[0].name = 'Forever';
arr2[1] = 'play games';
console.log('arr1===>', arr1); // arr1===> [{ name: 'Forever' },'see a film','write the code','play basketball', 'tourism']
console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']
- Array.prototype.slice()
// Array.prototype.concat() 实现浅拷贝
let arr1 = [
{
name: 'Chen'
},
'see a film',
'write the code',
'play basketball',
'tourism'
];
let arr2 = arr1.slice();
arr2[0].name = 'Forever';
arr2[1] = 'play games';
console.log('arr1===>', arr1); // arr1===> [{ name: 'Forever' },'see a film','write the code','play basketball', 'tourism']
console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']
当Array只有一层的时候,是深拷贝;所以当原数据进行浅拷贝,改变arr2的arr[1],而原数据arr1中的arr1[1]没有改变;
深拷贝的实现
- JSON.parse(JSON.stringify())
// JSON.parse(JSON.stringify())实现深拷贝Object
let obj1 = {
name: 'Chen',
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1 === obj2); // false
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: ['see a film', 'write the code', 'play basketball', 'tourism']}
console.log('obj2===>', obj2); // obj2===> { name: 'Forever',hobby: ['see a film', 'swim', 'alpinism', 'tourism']}
// JSON.parse(JSON.stringify())实现深拷贝Array
let arr1 = [
{
name: 'Chen'
},
'see a film',
'write the code',
'play basketball',
'tourism'
];
let arr2 = JSON.parse(JSON.stringify(arr1));
console.log(arr1 === arr2); // false
arr2[0].name = 'Forever';
arr2[1] = 'play games';
console.log('arr1===>', arr1); // arr1===> [{ name: 'Chen' },'see a film','write the code','play basketball', 'tourism']
console.log('arr2===>', arr2); // arr2===> [{ name: 'Forever' },'play games','write the code', 'play basketball', 'tourism']
😻 既然Object和Array可以通过JSON.parse(JSON.stringify())实现深拷贝,那么Date与Function可以实现嘛? 我们一起尝试以下,看看会有什么情况出现:
let fun1 = function() {
console.log('run~');
}
let fun2 = JSON.parse(JSON.stringify(fun1)) // undefined
JSON.parse(fun2) //Error: "undefined" is not valid JSON
let date1 = new Date();
let date2 = JSON.stringify(date1) // undefined
JSON.parse(date2) // Error: "undefined" is not valid JSON
JSON.parse() 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象 因此undefined不能被转换并抛出异常:"undefined"不是有效的 JSON;
**为什么function类型与Date类型转换成JSON后 会是undefined, 请参考链接:MDN(感兴趣的可以根据JSON序列化原理,手动实现一下JSON.stringify())
我们平时开发中将JSON.stringify应用最多的可能就是浅层的对象进行深拷贝,也就是进行序列化处理。但是当我们进行手撕代码的时候,需要考虑各种边界情况,这对于我们来说就比较麻烦,作为面试也是对数据类型的全面考察
- jQuery.extend()方法
// 需要引入jQuery库哦~
let obj = {
name: 'Chen',
hobby: [
'see a film',
'write the code',
'play basketball',
'tourism'
]
}
let obj1 = jQuery.extend(true, {}, obj);
console.log(obj === obj1); // false
obj2.name = 'Forever';
obj2.hobby[1] = 'swim';
obj2.hobby[2] = 'alpinism';
console.log('obj1===>', obj1); // obj1===> { name: 'Chen',hobby: ['see a film', 'write the code', 'play basketball', 'tourism']}
console.log('obj2===>', obj2); // obj1===> { name: 'Chen',hobby: ['see a film', 'swim', 'alpinism', 'tourism']}
- 手写递归方法:(递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝)
// 检测数据类型的功能函数
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;
}
调用一下手写递归实现深拷贝方法:🙊
const obj = {
name: 'Chen',
detail: {
age: '18',
height: '180',
bodyWeight: '68'
},
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
const obj1 = clone(obj);
console.log(obj1); // { name: 'Chen',detail: { age: '18', height: '180', bodyWeight: '68' }, hobby: [ 'see a film', 'write the code', 'play basketball', 'tourism' ]}
console.log(obj1 === obj); // false
循环引用
什么是循环引用: 一般指对象直接或间接地引用了自身;
循环引用一般分为下列几种情况:
- 父级引用:自身(obj)中的属性对应的值指向自己(obj);
- 同级引用:自身(obj)中某一属性对应的值 指向(引用)自身(obj);
- 相互引用:两个对象中的属性相互引用;
递归函数,看似已经解决了我们日常深拷贝的需要, 但是没有考虑到对象'循环引用'问题;
const obj = {
name: 'Chen',
detail: {
age: '18',
height: '180',
bodyWeight: '68'
},
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
obj.temp = obj; // obj中的属性temp的值指向了obj
const obj1 = clone(obj); // 报错:栈内存溢出
以上我们可以看出:
obj中新增属性temp属性引用obj, obj中的temp中的temp属性引用了obj,
这就构成了循环引用;clone函数中, 循环调用clone,从而造成一个死循环导致爆栈;
父级引用
:
const obj = {
name: 'Chen',
detail: {
age: '18',
height: '180',
bodyWeight: '68'
},
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
obj.temp = obj; // obj中的属性temp的值指向了obj
同级引用
:
const obj = {
name: 'Chen',
detail: {
age: '18',
height: '180',
bodyWeight: '68'
},
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
obj.detail['tempDetail'] = obj.detail; // obj.detail中的属性tempDetail的值指向了obj.detail
相互引用
:
const obj = {
name: 'Chen',
detail: {
age: '18',
height: '180',
bodyWeight: '68'
},
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
const obj1 = {
name: 'ChenYonx',
detail: {
age: '23',
height: '175',
bodyWeight: '70'
},
hobby: ['Watch the drama', 'Ride']
}
obj.tempDetail= obj1;
obj1.tempDetail = obj;
console.log('obj====>', obj);
console.log('obj1====>', obj1);
因此针对深拷贝的循环应用问题,对clone函数进行优化:
// 检测数据类型的功能函数
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;
}
调用一下优化后的clone(针对循环引用)
const obj = {
name: 'Chen',
detail: {
age: '18',
height: '180',
bodyWeight: '68'
},
hobby: ['see a film', 'write the code', 'play basketball', 'tourism']
}
obj.tempObj = obj;
const obj1 = clone(obj);
console.log('obj1=====>', obj1);
哈哈~ 可以完整的将循环引用的数据进行深拷贝下来
至此 深拷贝与浅拷贝就结束咯~
是我对js拷贝的理解了,在闲暇时光做的一个总结归纳~
想不到自己还是写完了~
希望对你有所帮助~