深拷贝我们需要考虑哪些事?

228 阅读7分钟

拷贝还是赋值?(深拷贝就是最小单元的赋值)

对于引用类型,我们不能通过看上去一样,从而去认为两个数组就是一样的。 比如:

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时 的流程:
  1. 在执行deepCopy时,先拷贝引用类型b,他等于deepCopy(obj['b']);把里面的值类型、引用类型都拷贝到res['b']上。先拷贝的是{a:100},此时的res就是{a:100}。那么,b这个属性的值是如何一步一步完成拷贝的呢?为何打印出来6次?
  2. 在拷贝obj['a']
  3. 拷贝完成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]}

双参数深拷贝小结

  1. 源对象与目标对象不互相影响,说明使用了不同的内存地址,是真正的值相同,内存地址也不同
  2. 我们整个deepCopy的函数,真正实现拷贝功能的就是如下代码
res[i]=obj[i]

把值类型的拷贝,把引用类型的属性一个一个拷贝。

  1. 当我们拷贝的对象中有一个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
  • 深拷贝考虑,更完善的类型,输出数据的类型,与输入数据的类型要兼顾
  • 深拷贝就是让内存地址不一样,所以我们需要开辟新的内存地址
  • 友情提示拷贝对象太大,很耗费内存
  • 深拷贝,就是让值相同,内存地址不同

思考

  1. 为何打印的次数是下图的

画了一个简易的数据表示图:

var A = {a:1,b:[1,2,{a:100}]};

码字不易,您的点赞是我前进的动力~

如有问题,欢迎指出~ (月下码农)