在JavaScript中,对于object和Array这类引用类型值,当从一 个变量向另一个变量复制引用类型值时,这 个值的副本其实是一个指针, 两个变量指向同一个堆对象,改变其中一个变量,另一个也会受到影响。 这种拷贝分为两种情况:拷贝引用和拷贝实例,也就是我们说的浅拷贝和深拷贝 。
一、数据类型
数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。
基本数据类型的特点:直接存储在栈(stack)中的数据
引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当JS引擎寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
二、浅拷贝与深拷贝
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
深拷贝和浅拷贝的示意图大致如下:
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
三、赋值和浅拷贝的区别
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
我们先来看两个例子,对比赋值与浅拷贝会对原对象带来哪些改变?
上面例子中,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:
四、浅拷贝的实现方式
1.Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
如果拷贝的属性是原始值类型,那么是深拷贝,改变不影响原对象。
如果拷贝的属性是引用类型,那么是浅拷贝,改变影响原对象。
注意:当object只有一层的时候,是深拷贝
2.Array.prototype.concat()
修改新对象会改到原对象:
关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
原数组的元素会按照下述规则拷贝:
如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
可能这段话晦涩难懂,我们举个例子,将上面的例子小作修改:
4.解构
let c={
age:1,
job:{
first:coding
}
}
let d={...c}
c.age='2'
console.log(d.age) //1
c.jobs.first='waiter'
console.log(d.jobs.first) //waiter
五、深拷贝的实现方式
- JSON.parse(JSON.stringify()) 但是识别不出来undefined
JSON.stringify()的作用是将 JavaScript 对象转换为 JSON 字符串,而JSON.parse()可以将JSON字符串转为一个对象。
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
缺陷:
1.会忽略undefined和symbol,如果属性的值是undefined,就忽略该属性了
2.这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
3.不能序列化函数
4.不能解决循环引用的对象
这是因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。
2.手写递归方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
let deepCopy = function (obj) {
if (typeof obj !== 'object') return;
let newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
let a={
b:{
c:'wn'
}
}
let d=deepCopy(a)
a.b.c='qqq'
console.log(d);
// {
// b:{
// c:'wn'
// }
// }
但上面的深拷贝方法遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈。如:
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
六、深拷贝函数改进(防止循环递归)
解决因循环递归而暴栈的问题,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可。
function deepClone(obj, parent = null){ // 改进(1)
let result = Array.isArray(obj)?[]:{};
let _parent = parent; // 改进(2)
while(_parent){ // 改进(3)
if(_parent.originalParent === obj){
return _parent.currentParent;
}
_parent = _parent.parent;
}
if(obj && typeof obj === "object"){
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(obj[key] && typeof obj[key] === "object"){
result[key] = deepClone(obj[key],{ // 改进(4)
originalParent: obj,
currentParent: result,
parent: parent
});
}else{
result[key] = obj[key];
}
}
}
}
return result;
}
// 调试用
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepClone(obj1);
console.log(obj1); //太长了去浏览器试一下吧~
console.log(obj2); //太长了去浏览器试一下吧~
随手点赞,将爱传递。哈哈哈哈哈哈哈哈哈