在说深拷贝与浅拷贝前,咱们先看两个简单的案例:javascript
//案例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}
按照常规思惟,obj1
应该和num1
同样,不会由于另一个值的改变而改变,而这里的obj1
却随着obj2
的改变而改变了。一样是变量,为何表现不同呢?这就要引入JS中基本类型和引用类型的概念了。java
ECMAScript变量可能包含两种不一样数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值彻底保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另外一个位置,该位置保存对象。
打个比方,基本类型和引用类型在赋值上的区别能够按“连锁店”和“单店”来理解:基本类型赋值等于在一个新的地方安装连锁店的规范标准新开一个分店,新开的店与其余旧店互不相关,各自运营;而引用类型赋值至关于一个店有两把钥匙,交给两个老板同时管理,两个老板的行为都有可能对一间店的运营形成影响。jquery
上面清晰明了的介绍了基本类型和引用类型的定义和区别。目前基本类型有:Boolean、Null、Undefined、Number、String、Symbol,引用类型有:Object、Array、Function。之因此说“目前”,由于Symbol就是ES6才出来的,以后也可能会有新的类型出来。git
再回到前面的案例,案例1中的值为基本类型,案例2中的值为引用类型。案例2中的赋值就是典型的浅拷贝,而且深拷贝与浅拷贝的概念只存在于引用类型。github
既然已经知道了深拷贝与浅拷贝的来由,那么该如何实现深拷贝?咱们先分别看看Array和Object自有方法是否支持:segmentfault
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
,看来深拷贝的实现并无那么难嘛。咱们把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]]
咦,arr2
又改变了arr1
,看来slice()只能实现一维数组的深拷贝。浏览器
具有同等特性的还有:concat、Array.from() 。函数
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: 2, y: {m: 2}}
经测试,Object.assign()也只能实现一维对象的深拷贝。post
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
(出如今数组中时)。
咱们再来把obj1
改造下:
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的深拷贝问题。只能祭出大杀器:递归
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)}
能够看到,递归完美的解决了前面遗留的全部问题,咱们也能够用第三方库:jquery的$.extend
和lodash的_.cloneDeep
来解决深拷贝。上面虽然是用Object验证,但对于Array也一样适用,由于Array也是特殊的Object。
到这里,深拷贝问题基本能够告一段落了。可是,还有一个很是特殊的场景:
循环引用拷贝
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); //太长了去浏览器试一下吧~
至此,已完成一个支持循环引用的深拷贝函数。固然,也可使用lodash的_.cloneDeep
噢~。
文章同步于如下社区,能够选一个关注我噢 。◕‿◕。