天天看点

js中的深拷贝深拷贝的实现

js深拷贝

    • 什么是浅拷贝?
    • 什么是深拷贝?
  • 深拷贝的实现
    • 最简单的方式
    • 基础方式
    • 基础方式修复
    • 解决引用自身问题

什么是浅拷贝?

新的对象复制已有对象中非对象属性的值和对象属性的引用就是浅拷贝。

不理解? 下面用 Object.assign 体现浅拷贝

var obj = {x: 1,y: 2,a: {z: 3}}

var obj1 = Object.assign({}, obj)
obj.a.z = 4
obj.x = 2
console.log(obj) // {x: 2, y: 2,  a: {z: 4}
console.log(obj1) // {x: 1, y: 2, a: {z: 4}}
           

从输出结果可以看出,浅拷贝时对非对象是会有新的内存地址的,而对对象的拷贝只是对已存在对象的属性引用,所以当原对象改变时,浅拷贝对象也会改变

什么是深拷贝?

深拷贝会另外拷贝一份一个一模一样的对象,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

同样的,我们使用 JSON.parse(JSON.stringify()) 来说明深拷贝

var obj = {x: 1,y: 2,a: {z: 3}}
var obj1 = JSON.parse(JSON.stringify(obj))

obj.x = 2
obj.a.z = 4
console.log(obj) // {x: 2, y: 2, a: {z: 4}}
console.log(obj1) // {x: 1, y: 2, a: {z: 3}}
           

由上面代码可以看出,深拷贝时,不管是对象还是非对象,都是完全的复制,而不是引用,是拷贝出了一个全新的对象,和原来对象并无关系

深拷贝的实现

最简单的方式

最简单的深拷贝实现方式,即为我们前面使用的方式 ---- JSON.parse(JSON.stringify())

function clone(target){
  return JSON.parse(JSON.stringify(target))
}
           

这个方法虽然是最简单的,但却是最实用的,因为大部分的场景都能用这条简单的语句解决。但显然,既然不能解决所有问题,那它就是不完善的。

基础方式

function clone1(target) {
  if(typeof target === 'object') {
    let cloneTarget = {}
    for(const key in target){
      cloneTarget[key] = clone1(target[key])
    }
    return cloneTarget
  }else {
    return target
  }
}
           

这里我们使用递归来处理深拷贝,如果是原始类型,直接返回;如果是引用类型,创建一个新对象,将对象内的属性深拷贝至新对象中,一直到最终的对象中为原始类型为止

基础方式修复

之前的基础方式有一个问题----我们并没有处理数组对象,如果对象是数组呢?

function newClone1(target) {
  if(typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {}
    for(const key in target){
      cloneTarget[key] = newClone1(target[key])
    }
    return cloneTarget
  }else {
    return target
  }
}
           

其实处理方式非常简单,只要在创建新对象时判断对象是否为数组。是,则创建数组对象

解决引用自身问题

如果使用之前的基础方式,其实是有问题的,如下:

var target = {a: 1, b: {c: 2}, c: [1, 2, 3, 4, 5]}
target .d = target 
let cloneTarget = newClone1(target)

// 报错信息如下
/*
Uncaught RangeError: Maximum call stack size exceeded
    at newClone1 (<anonymous>:1:19)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
    at newClone1 (<anonymous>:5:26)
*/
           

这是因为循环引用,递归永远出不来导致的栈溢出,那么,我们如何解决这个问题呢?

其实也比较简单,我们需要一个新的数据类型—Map

在 JavaScript 的对象,本质上是键值对的集合,但传统上只能使用字符串当做建,这就对它的使用产生了限制。

在ES6中,提供了 Map 数据结构。它也是键值对的集合,但是‘键’的范围不仅是字符串,Map提供的是‘值-值’。

并且,Map 有对应的 WeakMap,可以自动进行回收内存

考虑这种特性,我们可以对之前的拷贝进行完善

如果想详细了解 Map ,可以前往 http://es6.ruanyifeng.com/#docs/set-map

function clone2(target, map = new WeakMap()) {
  if(typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {}
    if(map.get(target)){
      return map.get(target)
    }
    map.set(target, cloneTarget)
    for(const key in target){
      cloneTarget[key] = clone2(target[key], map)
    }
    return cloneTarget
  }else {
    return target
  }
}
           

到这里,我们基本上算是解决了绝大部分的对象深拷贝。既然是绝大部分,那么说明还是存在一些问题

如果你还不满足于这样的深拷贝

推荐你去看这篇文章: https://juejin.im/post/5d6aa4f96fb9a06b112ad5b1

ps: 我的这篇文章基本是学习这位大牛