提及深拷贝和浅拷贝,首先咱们来看两个栗子数组
// 栗子1 var a = 1,b=a; console.log(a); console.log(b) b = 2; console.log(a); console.log(b) // 栗子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}
按照惯性思惟,栗子1中obj1应该跟a同样,不会因另一个值的改变而改变的啊,而这里倒是obj1跟着obj2的改变而改变了?一样都是变量,怎么就表现不同了呢?难道存在等级上的优劣?此处须要沉思一小会。要解决这个问题,就要引入一个JS中基本类型和引用类型的概念了。函数
ECMAScript变量包含两种不一样数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值彻底保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另外一个位置,该位置保存对象。
从上图能够看到,栈内存主要用于存储各类基本类型的变量,包括Boolean、Number、String、Undefined、Null等以及对象变量的指针。而堆内存主要负责对象Object这种变量类型的存储。目前基本类型有:
Boolean、Null、Undefined、Number、String、Symbol,引用类型有:Object、Array、Function。Symbol就是ES6才出来的,以后也可能会有新的类型出来。spa
让咱们再回到前面的案例,栗子1中的值为基本类型,栗子2中的值为引用类型,栗子2中的赋值就是典型的浅拷贝。咱们须要明确一点,深拷贝与浅拷贝的概念只存在于引用类型。指针
既然已经知道了深拷贝与浅拷贝的来由,那么该如何实现深拷贝?咱们分别来看看Array和Object自有方法是否支持:code
var arr1 = [1, 2]; var 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,看来深拷贝的实现并无那么难嘛。咱们把arr1改为二维数组再来看看结果对象
var arr1 = [1, 2, [3, 4]]; var 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]]
咦,arr2又改变了arr1,看来slice()只能实现一维数组的深拷贝,并不能实现真正的深拷贝。与之有同等特性的还有:concat、Array.from() 。递归
研究完Array,咱们来看看Objectip
var obj1 = {x: 1, y: 2}; var 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: 2, y: {m: 2}}
经实践证实,Object.assign()跟Array同样也只能实现一维对象的深拷贝。形成只能实现一维对象深拷贝的缘由是第一层的属性确实实现了深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址,因此才会形成上面的问题。内存
那怎么真正的实现引用类型的深拷贝呢?接下来要有请正主入场rem
1.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)) 简单粗暴,简简单单让你功力倍增,不过MDN文档的描述有句话写的很清楚:
undefined、任意的函数以及 symbol 值,在序列化过程当中会被忽略(出如今非数组对象的属性值中时)或者被转换成 null(出如今数组中时)。详情能够戳这里 MDN文档
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}
经实践证实,在将obj1进行JSON.stringify()序列化的过程当中,y、z、a都被忽略了,也就验证了MDN文档的描述。既然这样,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简单粗暴,已经知足90%的使用场景了。
通过验证,咱们发现JS 提供的自有方法并不能完全解决Array、Object的深拷贝问题。只能祭出大杀器:递归
2.递归
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)}
能够看到,递归完美的解决了前面遗留的全部问题。可是,还有一个很是特殊极端的场景:循环引用拷贝
var obj1 = { x: 1, y: 2 }; obj1.z = obj1; var obj2 = deepCopy(obj1);
此时若是调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而致使爆栈。解决这个问题也很是简单,只须要判断一个对象的字段是否引用了这个对象或这个对象的任意父级便可
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);