# 来来来,深拷贝和浅拷贝了解一下

javascript中的数据类型分为基本类型引用类型.我们平时所说的深浅拷贝都是针对引用类型而言的.对于基本类型,没有深浅拷贝的说法

# 浅拷贝

当我们在对象拷贝时,如果属性(假设为key)是对象(包括数组),这时我们传递的是一个地址(该地址指向key对象),即两个对象的属性值是同一个地址,指向了同一块内存空间.当通过其中一个对象去影响了这块内存空间的内容,相同的变化也会反映在另外一个对象上

# 复制引用

let obj = {
  a:1,
  b:{
    c:2
  }
}
// 浅拷贝实现函数
function shallowCopy(obj){
  if(typeof obj !== 'object'){
    return false
  }
  let o = Array.isArray(obj) ? [] : {}
  for(let p in obj){
    if(obj.hasOwnProperty(p)){
      o[p] = obj[p]
    }
  }
  return o
}
// 使用浅拷贝函数生成一个新的对象
let obj2 = shallowCopy(obj)
// a的属性值是基本类型,我们先来改变它
obj2.a = 2
console.log(obj.a)  // 1
console.log(obj2.a)  // 2
// 发现`obj2.a`的修改并不会影响到`obj.a`
console.log(obj.b.c)  // 2
console.log(obj2.b.c)  // 2
// 再来修改obj2.b的内容,这是一个对象
obj2.b.c = 3
console.log(obj.b.c)  // 3
console.log(obj2.b.c)  // 3
// 发现`obj2.b.c`的修改同步影响到了`obj.b.c`.这是因为`obj.b`和`obj2.b`指向的是同一块内存空间.相当于一个有着两扇门的房间,不管从哪头门进去,最终进去的都是同一个房间

// 我们再次修改`obj2.b`的内容,和上次的修改不太一样,这次我们重新赋值一个新的对象给它
obj2.b = {d:1}
console.log(obj.b)  // {c: 3}
console.log(obj2.b)  // {d: 1}
// 从上面的结果可以看出来,此时的`obj.b`与`obj2.b`之间已经不存在关联了,这是因为 obj2.b = {d:1} 这步操作赋值使得`obj2.b`和`obj.b`不再指向同一内存地址.相当于新造了一个房子,并且将连接原先房间的其中一头门拆下来,重新安装到这个新房子上

# Object.assign()

我们还可以使用Object.assign方法来实现函数的浅拷贝

let obj = {
  a:1,
  b:{
    c:2
  },
  fn:function(){}
}
let obj2 = Object.assign({},obj)
console.log(obj)  // {a: 1, b: {…}, fn: ƒ}
console.log(obj2)  // {a: 1, b: {…}, fn: ƒ}
obj.b.c = 5
console.log(obj.b.c)  // 5
console.log(obj2.b.c )  // 5

可以看出改变了obj.b.c的值,obj2.b.c的值也跟着变了

# concat()和slice()

这两个是数组的方法,都会返回一个新的数组,可以用来实现数组的浅拷贝.

let arr = [1,2,{a:1}]
let arr2 = arr.concat() // 或 let arr2 = arr.slice()
// 改变基本类型的值,不影响另外一个
arr[0] = 11
console.log(arr[0])  // 11
console.log(arr2[0])  // 1
// 改变了引用类型的值,会同步影响另外一个
arr[2].a = 2
console.log(arr[2].a)  // 2
console.log(arr2[2].a)  // 2

# 深拷贝(deepCopy)

深拷贝是一个与浅拷贝相对的概念,深拷贝就是要完完全全的拷贝对象了.实现深拷贝的核心是对于基本类型,直接赋值,对于引用类型,再次遍历,递归赋值

# JSON对象方法

对一个对象先用JSON.stringify方法对其序列化,然后再用JSON.parse方法反序列化,也能实现一定程度上的深拷贝.但是对象里面的函数会被过滤掉

let obj = {
  a:1,
  b:{c:2},
  fn:function(){}
}
let obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2)  // {a: 1, b: {c: 2}}

我们发现obj2中并没有包含fn,因为它在 JSON.stringify 这一步的时候就已经被过滤掉了. 我们在上面的代码后面再添加几句,如下

obj2.b.c = 3
console.log(obj.b.c)  // 2
console.log(obj2.b.c)  // 3

我们改变了obj2中属性值为对象的里面的内容,并没有影响到obj 还有一点就是,经过了JSON.parse(JSON.stringify)这么两级反转一波以后,新对象丢失了原来的constructor,其新的constructor都变成了Object

function Person(name){
  this.name = name
}
let p = new Person('zhangsan')
let p2 = JSON.parse(JSON.stringify(p))
console.log(p.constructor === Person)  // true
console.log(p2.constructor === Object)  // true

# 递归实现

下面我们使用递归的方式来实现一个深拷贝函数

let obj = {
  a:1,
  b:{
    c:2
  },
  fn:function(){}
}
// 深拷贝实现函数
function deepCopy(obj){
  if(typeof obj !== 'object'){
    return false
  }
  let o = Array.isArray(obj) ? [] : {}
  if(obj && typeof obj === 'object'){
    // for...in循环会遍历原型链上的属性
    for(let p in obj){
      // hasOwnProperty()用于判断一个对象自身是否具有指定的属性,并不包括原型链上的属性
      if(obj.hasOwnProperty(p)){
        if(obj[p] && typeof obj[p] === 'object'){
          o[p] = deepCopy(obj[p])
        }else{
          o[p] = obj[p]
        }
      }
    }
  }
  return o
}
let obj2 = deepCopy(obj)
console.log(obj)  // {a: 1, b: {…}, fn: ƒ}
console.log(obj2)  // {a: 1, b: {…}, fn: ƒ}
// 可以看出对象中的函数也被拷贝过来了

obj2.b.c = 3
console.log(obj2.b.c)  // 3
console.log(obj.b.c)  // 2
// 修改`obj2.b.c`并没有影响到`obj.b.c`

# 对象循环引用怎么办?

对象循环引用导致自己形成一个闭环,如下

let obj = {}
obj.obj = obj
let obj2 = deepCopy(obj)  // Maximum call stack size exceeded

假如我们的原对象是一个循环引用的对象,那么当我们用上面那个深拷贝函数进行拷贝的时候是会报红的 我们引入WeakMap结构来存储已经被拷贝的对象,每次拷贝前先查询该对象是否已经被拷贝,若已经被拷贝则取出对象返回,修改后的代码如下:

function deepCopy(obj, hash = new WeakMap()){
  if(typeof obj !== 'object'){
    return false
  }
  if(hash.has(obj)) return hash.get(obj)
  let o = Array.isArray(obj) ? [] : {}
  if(obj && typeof obj === 'object'){
    for(let p in obj){
      if(obj.hasOwnProperty(p)){
        hash.set(obj,o)
        if(obj[p] && typeof obj[p] === 'object'){
          o[p] = deepCopy(obj[p], hash)
        }else{
          o[p] = obj[p]
        }
      }
    }
  }
  return o
}

# 有特殊对象怎么办?

let obj = {
  d:new Date(),
  r:/\w/g
}
let obj2 = deepCopy(obj)
console.log(obj)
console.log(obj2)

发现在obj2中,d和r两个属性值都变成了{}空对象,说明此时的函数还无法拷贝某些特殊类型的对象,如Date,RegExp等.这时候就需要使用 结构化拷贝 了(constructor)

let obj = {
  d:new Date(),
  r:/\w/g
}
function deepCopy(obj, hash = new WeakMap()){
  if(typeof obj !== 'object'){
    return false
  }
  if(hash.has(obj)) return hash.get(obj)
  let o = Array.isArray(obj) ? [] : {}
  let C = obj.constructor
  if(obj && typeof obj === 'object'){
    switch(C){
      case Date:
        o = new C(obj)
        break;
      case RegExp:
        o = new C(obj)
        break;
      default:
    }
    for(let p in obj){
      if(obj.hasOwnProperty(p)){
        hash.set(obj,o)
        if(obj[p] && typeof obj[p] === 'object'){
          o[p] = deepCopy(obj[p], hash)
        }else{
          o[p] = obj[p]
        }
      }
    }
  }
  return o
}
let obj2 = deepCopy(obj)
console.log(obj2)  // 此时d和r两个属性都保留了原来的值 

上面的函数中只列举了DateRegExp这两种类型,若有其他类型需求的,可以自行添加上去

总结: 关于深浅拷贝的基本情况我们差不多已经了解了,但其实在深浅拷贝这个问题上还是有蛮多坑需要我们去踩的.在日常开发中,大家可以自行选择合适的拷贝方式,并不一定要用最全的那个.