目录
一、概念
要真正理解这两种拷贝,我们首先需要知道的是浅拷贝和深拷贝的概念只存在于引用数据类型中。
之前我们说过两种不同的数据类型——基本数据类型和引用数据类型,基本数据类型的特点是名值存储在栈内存中;而引用数据类型的特点是名存储在栈内存中,值存储于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值。
浅拷贝和深拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝:只复制指向某个对象的指针,而不复制对象本身,即新旧对象还是共享同一块内存。
例:
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时m.a的值是多少
m.a会输出15,因为这里的操作是浅拷贝,n和m指向的是同一个堆,对象复制只是复制对象的引用。
深拷贝:创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象的时候不会改到原对象。
例:
var m = { a: 10, b: 20 }
var n = {a:m.a,b:m.b};
n.a = 15;
// 这时m.a的值是多少
m.a的值仍然是10,没有发生改变。这是因为在深拷贝中,m对象和n对象是虽然所有的值都是一样的,但是在堆里面对应的不是同一个了。
示意图大致如下:
二、实现浅拷贝的方法
1、赋值
将对象的引用赋值。
例:
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时m.a的值是15,因为这里的操作是浅拷贝,n和m指向的是同一个堆,对象复制只是复制对象的引用。
2、Object.assign()
Object.assign()是ES6的新函数。Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
例:
// Object.assign()方法
var obj = {a:1,b:2};
var obj1 = Object.assign(obj);
obj1.a = 3;
console.log(obj.a);
//输出结果是:3
注意:当对象只有一层的时候,该函数实现的是深拷贝(在深拷贝那会举例)。
3、Array.prototype.concat()
例:
// Array.prototype.concat()方法
var arr = [1,2,{name:'小明'}];
var arr1 = arr.concat();
arr1[2].name = '小华';
console.log(arr);
console.log(arr1);
//根据可以发现我们在修改新对象的时候将原对象也改变了
结果如下:
4、Array.prototype.slice()
例:
// Array.prototype.slice()方法
var arr = [1,2,{name:'小花'}];
var arr1 = arr.slice();
arr1[2].name = '小白';
console.log(arr);
console.log(arr1);
//根据可以发现我们在修改新对象的时候将原对象也改变了
结果如下:
根据观察我们可以看到Array.prototype.concat()和Array.prototype.slice()这两种方法进行浅拷贝很相似,再修改新对象的时候都会改到原对象,但是需要注意的是:Array的slice和concat这两种方法是不修改原数组的,它们只是会返回一个浅拷贝了原数组中的元素的一个新数组。
三、实现深拷贝的方法
1、手动复制
例:
//手动复制
var obj1 = {a:1 , b:2 , c:3};
var obj2 = {a:obj1.a , b:obj1.b , c:obj1.c};
obj2.b = 8;
console.log(obj1);
// 可以根据输出结果看出obj1没有变化
console.log(obj2);
//可以根据输出结果看到obj2中b被改变
结果如下:
可以看到这种办法在对象层数过多时会很麻烦,层数少的时候使用比较合适。
2、Object.assign()<当对象只有一层>
前面讲到Object.assign()方法可以用来进行浅拷贝,但有一种特殊情况,当对象只有一层(即一维对象)时它进行的是深拷贝。
例:
// Object.assign()<对象只有一层时>
var obj1 = {a:4 , b:5 , c:6};
var obj2 = Object.assign({},obj1);
obj2.b = 8;
console.log(obj1);
// 可以根据输出结果看出obj1没有变化
console.log(obj2);
//可以根据输出结果看到obj2中b被改变
结果如下:
3、转成JSON再转回来
这种方法是用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成新的对象。
例:
//转成JSON再转回来
var obj1 = {x:{a:10} , y:2};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.x.a = 20;
console.log(obj1);
console.log(obj2);
console.log(obj1 === obj2);
console.log(obj1.x === obj2.x);
结果如下:
采用这种方法,可以产生新的对象,而且对象会开辟新的栈,实现深拷贝,方法简单易用。但是这种方法也有缺点,比如它会抛弃对象的constructor,也即深拷贝之后,不论这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象是无法通过这种方式深拷贝。只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。
undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
例:
//转成JSON再转回来局限性
var obj1 = {x:{a:10} , y:2 , z:undefined , function(){}};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.x.a = 20;
console.log(obj1);
console.log(obj2);
console.log(obj1 === obj2);
console.log(obj1.x === obj2.x);
//我们可以看出obj2中没有将obj1中的z还有函数打印出来
结果如下:
4、递归拷贝
上面讲到的2、3两种深拷贝方法都是JavaScript自带的方法,但是根据例子我们可以看出它们都具有一定的局限性,所以我们需要想办法自己实现深拷贝。
递归拷贝方法的原理是:遍历对象、数组直到里面都是基本数据类型,然后再去复制,就是深度拷贝。
例:
// 递归拷贝
function deepClone(initalObj, finalObj) {
var obj = finalObj || {};
for (var i in initalObj) {
var prop = initalObj[i]; // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
if(prop === obj) {
continue;
}
if (typeof prop === 'object') {
obj[i] = (prop.constructor === Array) ? [] : {};
arguments.callee(prop, obj[i]);
} else {
obj[i] = prop;
}
}
return obj;
}
var str = {};
var obj = { a: {a: "hello", b: 21} };
deepClone(obj, str);
console.log(obj);
console.log(str.a);
结果如下:
四、赋值、浅拷贝和深拷贝对原始数据的影响
操作 | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |