理解深拷贝和浅拷贝之前,先来看一下JavaScript的数据类型。
1、基本类型和引用类型
//案例1 var num1 = 1, num2 = num1; console.log(num1) //1 console.log(num2) //1 num2 = 2; //修改num2 console.log(num1) //1 console.log(num2) //2 //案例2 var obj1 = {x: 1, y: 2}, obj2 = obj1; console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 1, y: 2} obj2.x = 2; //修改obj2.x console.log(obj1) //{x: 2, y: 2} console.log(obj2) //{x: 2, y: 2}
ECMAScript 变量可能包含两种不同数据类型的值: 基本类型值 和 引用类型值。
基本类型值: 那些保存在栈内存中的简单数据段; 即这些值完全保存在内存中的一个位置。
引用类型值: 那些保存在堆内存中的对象;也就是说变量中保存的实际上是一个指针,这个指针指向内存中的另一个位置(保存对象)。
基本类型赋值等于在一个新的地方安装连锁店的规范标准新开一个分店,新开的店与其他旧店互不相关,各自运营;
而引用类型赋值相当于一个店有两把钥匙,交给两个老板同时管理,两个老板的行为都有可能对一间店的运营造成影响。
常见的基本数据类型: String、Number、Boolean、Null、Undefined、Symbol
常见的引用数据类型:Object、 Array、Function
2、深拷贝 与 浅拷贝
深拷贝 与 浅拷贝的概念只存在于 引用类型。
- 浅拷贝:只能实现一维对象的复制;当对象是多维时(例如:二维数组或对象),改变二维数组的值会影响原数组【因为二维数组值复制的是其引用】。
- 深拷贝:可以实现多维对象的复制。
(1)Array 和 Object 自身所带的深拷贝的方法
Array中具备深拷贝的方法: splice、concat、Array.from() --- 但是只能实现一维数组的深拷贝。
var arr1 = [1, 2], arr2 = arr1.slice(); console.log(arr1); //[1, 2] console.log(arr2); //[1, 2] arr2[0] = 3; //修改arr2 console.log(arr1); //[1, 2] console.log(arr2); //[3, 2]
二维数组的情况下,arr2的改变也会同时改变arr1:
var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice(); console.log(arr1); //[1, 2, [3, 4]] console.log(arr2); //[1, 2, [3, 4]] arr2[2][1] = 5; console.log(arr1); //[1, 2, [3, 5]] console.log(arr2); //[1, 2, [3, 5]]
Object.assign()也只能实现一维对象的深拷贝,并不能实现真正的深拷贝。
var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1); console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 1, y: 2} obj2.x = 2; //修改obj2.x console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 2, y: 2}
//二维对象 var obj1 = { x: 1, y: { m: 1 } }; var obj2 = Object.assign({}, obj1); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 2}} console.log(obj2) //{x: 1, y: {m: 2}}
造成只能实现一维对象深拷贝的原因:第一层的属性确实实现了深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址。
(2)JSON.parse(JSON.stringify(obj)) ,可是实现二维对象的深拷贝:
var obj1 = { x: 1, y: { m: 1 } }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 2, y: {m: 2}}
JSON.parse(JSON.stringify(obj)) 进行深拷贝是有局限性的;不能深拷贝还有 undefined、function、symbol值的对象。
var obj1 = { x: 1, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)} console.log(JSON.stringify(obj1)); //{"x":1} console.log(obj2) //{x: 1}
MDN文档中提示:undefined、任意的函数以及 symbol 值,
在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
(3)利用递归 来彻底解决深拷贝的问题
function deepCopy(obj) { // 创建一个新对象 let result = {} let keys = Object.keys(obj), key = null, temp = null; for (let i = 0; i < keys.length; i++) { key = keys[i]; temp = obj[key]; // 如果字段的值也是一个对象则递归操作 if (temp && typeof temp === 'object') { result[key] = deepCopy(temp); } else { // 否则直接赋值给新对象 result[key] = temp; } } return result; } var obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; var obj2 = deepCopy(obj1); obj2.x.m = 2; console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)} console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}
(4)非常特殊的情况---- 循环引用拷贝
var obj1 = { x: 1, y: 2 }; obj1.z = obj1; var obj2 = deepCopy(obj1);
此时如果调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而导致爆栈。jquery的$.extend也没有解决。
解决这个问题: 判断一个对象的字段是否引用了这个对象 或 这个对象的任意父类即可。
function deepCopy(obj, parent = null) { // 创建一个新对象 let result = {}; let keys = Object.keys(obj), key = null, temp= null, _parent = parent; // 该字段有父级则需要追溯该字段的父级 while (_parent) { // 如果该字段引用了它的父级则为循环引用 if (_parent.originalParent === obj) { // 循环引用直接返回同级的新对象 return _parent.currentParent; } _parent = _parent.parent; } for (let i = 0; i < keys.length; i++) { key = keys[i]; temp= obj[key]; // 如果字段的值也是一个对象 if (temp && typeof temp=== 'object') { // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用 result[key] = DeepCopy(temp, { originalParent: obj, currentParent: result, parent: parent }); } else { result[key] = temp; } } return result; } var obj1 = { x: 1, y: 2 }; obj1.z = obj1; var obj2 = deepCopy(obj1); console.log(obj1); //太长了去浏览器试一下吧~ console.log(obj2); //太长了去浏览器试一下吧~
(5)当然也可以使用第三方库
jquery的$.extend 和 lodash的_.cloneDeep 来解决深拷贝的问题。
3、总结
- 简单的一维层次的拷贝可以利用数组自身方法和对象的Object.assign实现,在二维层次上方法失效,无法实现深拷贝
- 简单粗暴的常见的拷贝可以通过JSON.parse(JSON.stringify(obj))实现,但对于属性的某些特殊类型的值失效。
- 终极方法,用递归实现引用类型的深拷贝
- 当然还有其他方法,比如使用第三方库内封装的方法