JavaScript:简单粗暴浅拷贝,钻牛角尖深拷贝

234 阅读5分钟

深拷贝和浅拷贝都和对象的复制有关,它们是对象复制的两种不同方式。接下来就来看一下,浅拷贝是怎么个浅法,还有深拷贝深在哪里。

简单粗暴浅拷贝

来个栗子

先复制一个对象看看:

let a = {
  name: 'a',
  age: 99,
  car: {
    color: 'blue'
  }
}

let b = {}

Object.keys(a).forEach((key) => {
  b[key] = a[key]
})

console.log(b)
// {
//   name: 'a',
//   age: 99,
//   car: {
//     color: 'blue'
//   }
// }

首先声明了一个对象 a 还有一个空对象 b. 然后遍历对象 a 的可枚举属性,将属性和属性值一一分配给 b. 这里,实现了对象的复制。

接下来,对 b 进行一些操作:

b.name = 'b'
b.car.color = 'red'

console.log(b)
// {
//   name: 'b',
//   age: 99,
//   car: {
//     color: 'red'
//   }
// }

console.log(a)
// {
//   name: 'a',
//   age: 99,
//   car: {
//     color: 'red'
//   }
// }

上面的代码,将 bname 属性值变成 'b', 然后再将 bcar 属性的 color 变为 'red'.

打印 b,发现这些改动都预期发生了。

打印 a, 出现了一些意料之外的事情。aname 仍然是 'a' ,然而 acar 属性的 color 属性却发生了变化,变得和刚刚赋给 b 的一样了,成了 'red'.

这是怎么回事呢?

剧情解析

时光稍微倒流一下,回到复制对象的地方看一看。复制的关键代码:

Object.keys(a).forEach((key) => {
  b[key] = a[key]
})

代码把 a 的可枚举属性悉数分配给 b.

分配 car 属性的时候,慢动作细节即为:

b['car'] = a['car']

a['car'] 的值是一个对象,准确地说,a['car'] 的值是一个指针,指向了对象 {color: 'blue'}

在 JavaScript 当中,把对象赋给一个值,实际上是把对象的内存地址赋给这个值,然后,这个值就指向了该对象。对象是引用类型,不同于基本类型,引用类型存放在堆中。

当把 a['car'] 的值赋给 b['car'] 之时,其实是把这个指针复制了一份,然后赋给 b['car'],最终, b['car'] 也指向了同一个对象。

后来执行 b.car.colorb.car 指向的对象进行操作,a.car 也产生了变化,因为它们指向的就是同一个对象。

上面这个例子,就属于浅拷贝。

归纳一下

浅拷贝,简单粗暴,只管复制,不考虑属性值是不是指向对象

可能上面这个归纳,在这个时候,还是有点模糊,感觉不甚清晰,接下来,来个深拷贝对比一下。

钻牛角尖深拷贝

来个栗子

先上代码:

function deepCopy (target, source) {
  Object.keys(source).forEach((key) => {
    if (getType(source[key]) === 'Object') {
      target[key] = {}
      deepCopy(target[key], source[key])
    } else if (getType(source[key]) === 'Array') {
      target[key] = []
      deepCopy(target[key], source[key])
    } else {
      target[key] = source[key]
    }
  })
  return target
}

// Get type of parameter val.
function getType(val) {
  let reg = /^\[object\s(\w*)\]$/
  return reg.exec(Object.prototype.toString.call(val))[1]
}

var a = {
  name: 'a',
  age: 99,
  car: {
    color: 'blue'
  }
}

let b = {}

deepCopy(b, a)

console.log(b)
// {
//   name: 'a',
//   age: 99,
//   car: {
//     color: 'blue'
//   }
// }

上面的代码实现了一个深拷贝函数 deepCopy. 这个函数接受两个参数,target 是目标对象,source 是源对象,函数内将源对象的属性悉数拷贝到目标对象。

函数中首先遍历源对象的属性,分三种情况来处理,分别是当属性值指向对象,指向数组,或者是其他情况。

第一种情况,当 source[key] 指向对象时,先赋一个空对象给 target[key],然后再以 target[key] 这个空对象为 target 参数,以 source[key]source 参数,递归调用 deepCopy.

第二种情况,当 source[key]指向数组,就赋一个空数组给 target[key],接下来以 target[key] 作为 target 参数,以 source[key]source 参数,递归调用 deepCopy.

第三种情况,当 source[key] 不是对象也不是数组,那么直接把 source[key] 的值赋给 target[key].

遍历结束,deepCopy 返回 target.

接着,声明了函数 getType, 就是 deepCopy 里面用到的判断数据类型的函数。getType 接受一个参数 val, 并返回 val 的类型。函数内利用 Object.prototype.toString 得到 val 的类型,但这样得到的是类似 '[object String]'以及 '[object Array]' 这样的字符串,还需要加工一下,才能拿到代表类型的字符串,所以声明了一个正则表达式,来进行从 '[object String]' 提取出 'String' 这样的工作。

然后,声明对象 a, 接着,声明空对象 b, 再执行 deepCopy(b, a)a 深拷贝到 b.

console.log(b) 可知 b 已经成功拿到 a 的可枚举属性。

接下来,对 b 进行一些操作:

b.name = 'b'
b.car.color = 'red'

console.log(b)
// {
//   name: 'b',
//   age: 99,
//   car: {
//     color: 'red'
//   }
// }

console.log(a)
// {
//   name: 'a',
//   age: 99,
//   car: {
//     color: 'blue'
//   }
// }

像第一个例子一样,将 bname 属性赋值为 'b', bcar 属性的 color 赋值为 'red'.

打印 b, 发现改变已经生效。

打印 a, 可见 a 没有像上一个例子那样发生改变。

剧情解析

在这个例子中,当遍历到 a[car] 属性的时候,发现属性值指向的是一个对象,就将一个空对象赋给 b[car], 再把 a[car] 所指向的对象当做 source, b[car] 指向的空对象当做 target, 执行 deepCopy 操作,将 a[car] 所指的对象属性拷贝给 b[car] 所指的空对象。这个时候, a[car]b[car] 所指的并非同一个对象,所以,任凭怎么操作 b[car], a[car] 都不会受到丝毫影响。

归纳一下

这就是深拷贝,深拷贝在遍历源对象属性的时候,遇到可遍历的,如对象和数组,会再深入遍历其属性,并进行拷贝

总结

从上面的例子可以知道,深拷贝和浅拷贝,区别在于,在遍历源对象的时候,遇到可遍历的属性值,会不会去进行遍历。浅拷贝是直接将属性值复制过去,而深拷贝则会继续深入遍历并拷贝。