拷贝还是赋值?(深拷贝就是最小单元的赋值)
对于引用类型,我们不能通过看上去一样,从而去认为两个数组就是一样的。 比如:
var arr1=[1,2,3]
var arr2=[...arr]
console.log(arr2);
//[1, 2, 3]
arr2===arr
//false
上面的例子中,看上去,b和arr是一样的。但是实际上他两是指向的不同的堆内存地址。所以他两是独立的不一样的。
引用类型比较的是内存地址
var arr=[1,2,3];
var b=arr;
console.log(b); //[1, 2, 3]
b===arr
//true
上面的例子中,arr和b不仅仅是看上去一样,并且指向了同样的内存地址。所以他两===。
浅拷贝
什么是浅拷贝?
function deepCopy(obj){
var res = obj.constructor === Array ? [] :{};
for(var key in obj){
res[key]=obj[key]
}
return res;
};
var o={a:1,b:[1,2,3]};
var res = deepCopy(o);
//
res===o
//false
//分别修改res中的a和b中的第一个值
res.b[0]=100
res.a=20
// o对象中的值类型已经独立了内存空间,而引用类型还是会互相影响
console.log(o,res);// {a:1,b:[100,2,3]} {a:20,b:[100,2,3]}
我们拷贝一个对象和直接赋值一个对象的区别:
var o={a:1,b:[1,2,3]};
var o1=o;
o===o1; // true
//修改后者,前者也会受到影响。
//无论你修改的是对象当中的什么类型(值类型、引用类型)
o1.a=100
console.log(o); // {a: 100, b: Array(3)}
浅拷贝常用方法:
- slice(0)
- [...arr]
- assgin
浅拷贝小结
- 对象中的值类型已经独立了内存空间,而引用类型还是会互相影响。
- 浅拷贝与直接赋值的区别:后者是无论任何类型修改都会互相影响。而前者做到了对象中的值类型不在受到影响。
深拷贝是什么?
深拷贝就是两个对立的对象,他两看上去一样,但是实际他两不一样。各自修改后,互不影响。更加直观的说就是,把一个对象上的每一个最小单元的值都做一次赋值操作。
深拷贝就是把对象中的所有值的最小单元,进行赋值给另一个对象。
A和B看上去一模一样
A不影响B, B也不影响A
深拷贝的实现1(把浅拷贝加一个递归)
简易版本的深拷贝,可以完全复制一个对象,包括对象的原型对象和方法,不包括Map和Set,RegExp, Date等
不想拷贝原型上的属性,使用o.hasOwnProperty(key)
function deepCopy(obj){
var res = obj.constructor === Array ? [] :{};
for(var key in obj){
if(typeof obj[key] === "object"){
res[key]=deepCopy(obj[key])
}else{
res[key]=obj[key]
}
}
return res;
};
var A = {a:1,b:[1,2,3]};
A.prototype={c:1,d:function(){console.log('d')}}
var B = deepCopy(A);
//我们分别修改了B对象上的值类型和引用类型的数据
B.a=100;
B.b[0]=200;
console.log(A,B);
// {a:1,b:[1,2,3],prototype: {c: 1, d: ƒ}}
// {a:100,b:[200,2,3],prototype: {c: 1, d: ƒ}}
我们发现,此时A和B已经不在互相影响了,无论是任何类型,我们最终都走了一对一的赋值操作,而且是最小单元的。比如,数组中的每一项。
对象中的数组,其实我们是做了如下的操作:
res[key]=obj[key] 也就是最小单元的赋值操作。
function deepCopy(obj){
var res = obj.constructor === Array ? [] :{};
for(var key in obj){
res[key]=obj[key]
}
return res;
};
var a =[1,2,[3]];
var b =deepCopy(a);
a===b; // false
b[2]=[100];
console.log(a,b); // [1,2,[3]] [1, 2, [100]]
深拷贝代码执行流程分析
function deepCopy(obj){
var res = obj.constructor === Array ? [] :{};
for(var key in obj){
if(typeof obj[key] === "object"){
console.log(deepCopy(obj[key]));
res[key]=deepCopy(obj[key]);
}else{
res[key]=obj[key]
}
}
console.log(res);
return res;
};
var A = {a:1,b:[1,2,{a:100}]};
var B = deepCopy(A);
res 只是一个暂存的单元,遇到值是对象时他就是对象,遇到值时数组时,他就是数组。
拷贝A时 的流程:
- 在执行deepCopy时,先拷贝引用类型b,他等于deepCopy(obj['b']);把里面的值类型、引用类型都拷贝到res['b']上。先拷贝的是{a:100},此时的res就是{a:100}。那么,b这个属性的值是如何一步一步完成拷贝的呢?为何打印出来6次?
- 在拷贝obj['a']
- 拷贝完成obj上的a 和 b 两个属性。
双参数深拷贝
function deepCopy(obj,res){
var res = res || {};
for(var i in obj){
if(typeof obj[i] == "object"){
res[i]=Array.isArray(obj[i]) ? [] :{};
deepCopy(obj[i],res[i]);
}else{
res[i]=obj[i];
}
}
return res;
};
var A = {a:1,b:[1,2,3]};
var b=deepCopy(A);
b.b[0]=100;
console.log(b); // {a:1,b:[100,2,3]}
console.log(A); // {a:1,b:[1,2,3]}
双参数深拷贝小结
- 源对象与目标对象不互相影响,说明使用了不同的内存地址,是真正的值相同,内存地址也不同
- 我们整个deepCopy的函数,真正实现拷贝功能的就是如下代码
res[i]=obj[i]
把值类型的拷贝,把引用类型的属性一个一个拷贝。
- 当我们拷贝的对象中有一个key的value是数组或者对象时,或者更深处的值是引用类型时,如 obj[i],obj[i][0],是一个引用类型时,我们直接进行递归调用。前提是我们要把返回的value设置成对应的数据格式。比如如上代码。
深拷贝的实现2(类型更加完整)
类型更加完整的深拷贝,可以完全复制一个对象,包括对象的原型对象和方法,包括Map和Set,但是没有考虑,循环引用,爆栈等
对了,加一个参数容错吧!对于入参容错判断,一定要用typeof 不要用toString
if(Object.prototype.toString.call(obj) != "[object object]"){return false }
以上的容错,对于除了对象来说的数据类型,尤其是用的比较少的Map和Set 是灾难性的。
function deepCopy(obj){
if(typeof obj != "object"){return false }
var res;
switch(obj.constructor){
case Array:
res = [];
obj.map((item,index)=>{
res[index] = item instanceof Object ? deepCopy(item) : item
})
break;
case Object:
res = {};
for(var key in obj){
res[key] = typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key]
}
break;
case Map:
res = new Map();
obj.forEach((item,index)=>{
res.set(index, typeof item === "object" ? deepCopy(item) : item)
})
break;
case Set:
res = new Set;
obj.forEach((item,index)=>{
res.add(typeof item === "object" ? deepCopy(item) : item)
})
break;
default:
throw new Error('仅支持[],{},Map,Set')
}
return res;
};
var A = {a:1,b:[1,2,3],c:new Map([['a',1],['b',2]]),d:new Set([[1, 2, 3, 4, 5]])};
var B = deepCopy(A);
A===B;//false
//修改A上的数组,Map,Set
A.c.set("c",3);
A.d.add(666);
A.b[3]=4;
//打印
console.log(A,B);
// {a: 1, b: Array(4), c: Map(3), d: Set(2)}
// {a: 1, b: Array(3), c: Map(2), d: Set(1)}
// 通过数量我们已经看出。A和B不再互相影响。
类型判断
考虑到各种属性值的问题,类型判断用typeof 和instansof 都OK
new Map instanceof Object
(2).constructor === Number; // true
[].constructor === Array; // true
小提示:
switch([].constructor){
case Object:
console.log("is {}");
break;
case Array:
console.log("is []");
break;
case Number:
console.log("is Number");
break;
default:
console.log("is other");
break;
}
// is []
//不要忘记加break
switch([].constructor){
case Array:
console.log("is []");
case Object:
console.log("is {}");
default:
console.log("is other")
}
// is []
// is {}
// is other
不要忘记加break
深拷贝的实现3(类型更加完整+解决循环引用)
先看下,我们要用到的一些方法:
js中遍历一个对象的属性的方法
- Object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括Symbol属性
- Object.getOwnPropertyNames() 返回自身的可枚举和不可枚举属性。但是不包括Symbol属性
- Object.getOwnPropertySymbols() 返回自身的Symol属性
- for...in 可以遍历对象的自身的和继承的可枚举属性,不包含Symbol属性
- Reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是Symbol。注意不包括继承的属性
//今天早一点休息,争取2点前休息,改天再续
请参见:https://juejin.im/post/5dd0caea6fb9a01fe736b186
以上深拷贝的实现从1到N,功能递增
深拷贝使用场景
- 表单数据缓存
- 其他
深拷贝最不靠谱的方法
- JSON.parse(JSON.stringify(obj))
总结
- 深拷贝就是最小单元的赋值
- 对于入参容错判断,一定要用typeof 或者 instanceof不要用toString
- A和B看上去一模一样,A不影响B, B也不影响A
- 深拷贝考虑,更完善的类型,输出数据的类型,与输入数据的类型要兼顾
- 深拷贝就是让内存地址不一样,所以我们需要开辟新的内存地址
- 友情提示拷贝对象太大,很耗费内存
- 深拷贝,就是让值相同,内存地址不同
思考
- 为何打印的次数是下图的
画了一个简易的数据表示图:
var A = {a:1,b:[1,2,{a:100}]};
码字不易,您的点赞是我前进的动力~
如有问题,欢迎指出~ (月下码农)