复杂数据深拷贝和浅拷贝方法

1,401 阅读6分钟

本文不深究js数据类型的存储,以下探讨仅在了解底层原理基础之上

深拷贝

深拷贝:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

以下不支持,源数组的属性值是一个 json 对象时,并且直接使用索引修改某一 json 属性这种情况(不支持数组索引修改)

类似这样:

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]
经过copy i 得到 c 之后
c[0].kk = 3

以下支持,源数组的属性值是一个 json 对象时,并且直接使用索引修改某一 json 属性这种情况

  • lodash 引入 _.cloneDeep 完美解决

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = _.cloneDeep(i)

console.log('相等吗==>', i === c)      // false

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

  • JSON.parse 和 JSON.stringify 组合使用,可以解决 (有弊端)


let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = JSON.parse(JSON.stringify(i))

console.log('相等吗==>', i === c)      // false

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

弊端如下:

它的主要缺点是,只限于处理可被 JSON.stringify() 编码的值。

JSON.stringify() 将编码 JSON 支持的值。包含 Boolean,Number,String,以及对象,数组。其他任何内容都将被特殊处理。

undefined,Function,Symbol 时,它被忽略掉 Infinity,NaN 会被变成 null Date 对象会被转化为 String (默认调用date.toISOString())

问:为什么JSON.stringify() 编码 JSON 支持的值那么少呢?

因为JSON是一个通用的文本格式,和语言无关。设想如果将函数定义也stringify的话,如何判断是哪种语言,并且通过合适的方式将其呈现出来将会变得特别复杂。特别是和语言相关的一些特性,比如JavaScript中的Symbol。

  • 自己造轮子,完美解决深拷贝

如果源数组是个对象的引用,也不会拷贝这个对象的引用到新的数组。(支持数组索引修改)

function copy(obj) {
    if (!obj || typeof obj !== 'object') {
        return
    }
    var newObj = obj.constructor === Array ? [] : {}
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object') {
                newObj[key] = copy(obj[key])
            } else {
                newObj[key] = obj[key]
            }
        }
    }
    return newObj
}



let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = copy(i)

console.log('相等吗==>', i === c)      // false

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ] 

浅拷贝

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

  • Object.assign(target, source)

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = Object.assign(i)

console.log('相等吗==>', i === c)      // true

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ] 
  • 直接赋值(=)

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = i

console.log('相等吗==>', i === c)      // true

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2, kk: 3 }, { aa: 1, bb: 2 } ] 
  • [...] es6解构赋值

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let [...c] = i
console.log('相等吗==>', i === c)    false  

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]
console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2, kk: 3  }, { aa: 1, bb: 2 } ]
console.log('c==>', c)      //  [ { aa: 1, bb: 2, kk: 3  }, { aa: 1, bb: 2 } ]

疑问:既然是深拷贝,为什么面对通过数组索引修改属性的时候就会影响到原来数据呢?

实际上这种 copy 方法只是拷贝了最外层的数据,对于包含的内层对象还是修改的是栈内存中原对象的副本,并没有修改指向堆内存中的同一个对象的指针。(粗略解释,可以戳文末详细了解一下)

  • concat()

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = i.concat()

console.log('相等吗==>', i === c)      false

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]   
c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2, kk: 3  }, { aa: 1, bb: 2 } ]
console.log('c==>', c)      //  [ { aa: 1, bb: 2, kk: 3  }, { aa: 1, bb: 2 } ]

  • slice

let i = [{
    aa: 1,
    bb: 2
},{
    aa: 1,
    bb: 2
}]

let c = i.slice(0)

console.log('相等吗==>', i === c)      // false

c.kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 } ]

console.log('c==>', c)      //  [ { aa: 1, bb: 2 }, { aa: 1, bb: 2 }, kk: 3 ]

c[0].kk = 3

console.log('i==>', i)      //  [ { aa: 1, bb: 2, kk: 3  }, { aa: 1, bb: 2 } ]
console.log('c==>', c)      //  [ { aa: 1, bb: 2, kk: 3  }, { aa: 1, bb: 2 } ]

  • 自己造轮子,完美解决浅拷贝

function copy(obj) {
  if (!obj || typeof obj !== 'object') {
    return
  }

  var newObj = obj.constructor === Array ? [] : {}
  for (var key in obj) {
    newObj[key] = obj[key]
  }
  return newObj
}

深拷贝和浅拷贝的区别与解释

在javaScript中,有两种数据类型,一种是基本数据类型,一种是引用数据类型。

基本数据 类型是按值访问,也就是说在操作基本数据类型时,直接修改的是变量的值。

引用类型 呢,直接按引用来访问的。也就是说js的引用类型(对象类型),是保存在内存中的,JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间,所以我们实际操作的是对象的引用。

因此们赋值对象的时候,是将对象的引用赋值给了另一个对象,实际上变量指的是同一个内存地址中对象的值,这就使得改变其中一个对象的值,另一个也会跟着改变,这就是 浅拷贝

深拷贝呢,会开辟一个新的内存地址来存放新的对象的值,两个对象对应两个不同的地址,修改其中一个另一个不会受到影响

总结

-- 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

参见 sunshine小小倩 的博客详细了解 js数据类型的存储