什么是浅拷贝?
浅拷贝:复制一层对象的属性,并不包括对象里面的为引用类型的数据,当改变拷贝的对象里面的引用类型时,源对象也会改变。
什么是深拷贝?
深拷贝:重新开辟一个内存空间,需要递归拷贝对象里的引用,直到子属性都为基本类型。两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
如何区分深拷贝与浅拷贝?简单来说,假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,如果B没变,那就是深拷贝。
PS:深拷贝和浅拷贝一般只针对像 Object, Array 这样的复杂对象的。
所以简单来说,浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级。
文中会简单阐述js的基本数据类型与引用数据类型,为了能更好的理解深拷贝与浅拷贝,我们引入了栈堆概念。
javaScript的变量类型
1.ECMAScript 中的变量类型分为两类:
- 基本类型:
undefined,null,布尔值(Boolean),字符串(String),数值(Number)
,变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。 - 引用类型: 统称为
Object类型
,细分的话,有:Object类型,Array类型,Date类型,Function类型
等。存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
JavaScript
存储对象都是存地址的,所以浅拷贝会导致 arr1 和 arr2 指向同一块内存地址。改变了其中一方的内容,都是在原来的内存上做修改会导致拷贝对象和源对象都发生改变,而深拷贝是开辟一块新的内存地址,将原对象的各个属性逐个复制进去。对拷贝对象和源对象各自的操作互不影响。
2、不同类型的存储方式:
基本数据类型 名值 存储在 栈内存,形式如下:栈内存中分别存储着变量的标识符以及变量的值。
即
var a = "A";
在栈内存中是这样的
引用类型 名 存储在栈内存中,值 存储在堆内存中 , 但是栈内存会提供一个引用的地址指向堆内存中的值,当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需的数据。
即
var a = {name:“jack”};
在内存中是这样的
3、不同类型的复制方式:
基本类型 的复制:当你在复制基本类型的时候,栈内存会新开辟一个内存,相当于把值也一并复制给了新的变量。
栗子 1:
var a = 1;
var b = a;
console.log(a === b);
var a = 2;
console.log(a);
console.log(b);
内存中是这样的:
var a = 1;
var b = a;
a = 2;
改变 a 变量的值,并不会影响 b 的值。因为此时的b已自食其力,翅膀硬了,不受a的影响了。当然,let a=1,b=a;虽然b不受a影响,但这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object类型数据。
引用类型 的复制:当你在复制引用类型的时候,实际上只是复制了指向堆内存的引用地址,并非堆里面的值。即原来的变量与复制的新变量指向了同一个堆里的值。
栗子 2:
var a = {name:"jack",age:20};
var b = a;
console.log(a === b);
a.age = 30;
console.log(a);
console.log(b);
改变 a 变量的值,会影响 b 的值。
内存中是这样的:
var a = {name:"jack",age:20};
var b = a;
a.age = 30;
而当我们a.age=30时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。
深拷贝 & 浅拷贝
由此可以明白,所谓浅拷贝:
对于仅仅是复制了引用(地址),换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为 浅拷贝。
而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为 深拷贝。
深浅拷贝 的主要区别就是:复制的是引用(地址)还是复制的是实例。
所以上面的 栗子2 ,如何可以变成深拷贝呢?
我们可以想象出让 b 在内存中像下图这样,肯定就是深拷贝了。
那么代码上如何实现呢?
利用 递归 来实现深复制,对属性中所有引用类型的值,遍历到是基本类型的值为止。
function deepClone(source){
if(!source && typeof source !== 'object'){
throw new Error('error arguments', 'shallowClone');
}
var targetObj = Array.isArray(source) ? [] : {};
for(var keys in source){
if(Object.prototype.hasOwnProperty.call(source,keys)){
if(source[keys] && typeof source[keys] === 'object'){
targetObj[keys] = deepClone(source[keys]); //递归
}else{
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
function deepClone(data){
if(!data || !(data instanceof Object) || (typeof data=="function")){
return data||undefined;
}
var constructor = data.constructor;
var result = new constructor();
for(var key in data){
if(Object.prototype.hasOwnProperty.call(data,key)){
result[key]=deepClone(data[key]);
}
}
return result;
}
检测一下
var a = {name:"jack",age:20};
var b = deepClone(a);
console.log(a === b);
a.age = 30;
console.log(a);
console.log(b);
最后让我们来看看 一些 js 中的 复制方法,他们到底是深拷贝还是浅拷贝?
1、 Array 的 slice 和 concat 方法
两者都会返回一个新的数组实例。
栗子:
slice:
var a = [1,2,3];
var b = a.slice(); //slice
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);
concat:
var a = [1,2,3];
var b = a.concat(); //concat
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);
看到结果,如果你觉得,这两个方法是深复制,那就恭喜你跳进了坑里
让咱们再看一个颠覆你观念的栗子:
var a = [[1,2,3],4,5];
var b = a.slice();
console.log(a === b);
a[0][0] = 6;
console.log(a);
console.log(b);
嗯?都变啦?
这就是坑,知道吗?
所以 Array的 slice 和 concat 方法 并不是 真正的深拷贝,他们其实是浅拷贝。
2、 jQuery中的 extend 复制方法
可以用来扩展对象,这个方法可以传入一个参数:deep(true or false),表示是否执行深复制(如果是深复制则会执行递归复制)。
栗子:
深拷贝:
var obj = {name:'xixi',age:20,company : { name : '腾讯', address : '深圳'} };
var obj_extend = $.extend(true,{}, obj); //extend方法,第一个参数为true,为深拷贝,为false,或者没有为浅拷贝。
console.log(obj === obj_extend);
obj.company.name = "ali";
obj.name = "hei";
console.log(obj);
console.log(obj_extend);
浅拷贝:
var obj = {name:"xixi",age:20};
var obj_extend = $.extend(false,{}, obj); //extend方法,第一个参数为true,为深拷贝,为false,或者没有为浅拷贝。
console.log(obj === obj_extend);
obj.name = "heihei";
console.log(obj);
console.log(obj_extend);
咦,company 的变化 可以看出 深浅复制来(即箭头所指), 红色方框圈出的地方,怎么和上面 slice
和 concat
的情况一样?难道也是羊?
3、JSON 对象的 parse 和 stringify
JOSN 对象中的 stringify 可以把一个 js 对象序列化为一个 JSON 字符串,parse 可以把 JSON 字符串反序列化为一个 js 对象,这两个方法实现的是深拷贝。
栗子:
var obj = {name:'xixi',age:20,company : { name : '腾讯', address : '深圳'} };
var obj_json = JSON.parse(JSON.stringify(obj));
console.log(obj === obj_json);
obj.company.name = "ali";
obj.name = "hei";
console.log(obj);
console.log(obj_json);
jQuery.extend( [deep ], target, object1 [, objectN ] )
deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝
target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。
object1 objectN可选。 Object类型 第一个以及第N个被合并的对象。
let a=[0,1,[2,3],4],
b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
可以看到,效果与上面方法一样,只是需要依赖JQ库。
4.Object.assign()
基本用法:**Object.assign()**
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign()
方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果只有一个参数,Object.assign会直接返回该参数。
Object.assign()
方法既不属于深拷贝,也不属于浅拷贝。对于一级的时候,是深拷贝,如果是二级以后对象就是浅拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
5.总结一下:
Array
的 slice
和 concat
方法 和 jQuery
中的 extend
复制方法,他们都会复制第一层的值,对于 第一层 的值都是 深拷贝,而到 第二层 的时候 Array
的 slice
和 concat
方法就是 复制引用 ,jQuery
中的 extend
复制方法 则 取决于 你的 第一个参数, 也就是是否进行递归复制。所谓第一层 就是 key 所对应的 value 值是基本数据类型,也就像上面栗子中的name、age,而对于 value 值是引用类型 则为第二层,也就像上面栗子中的 company 。