浅拷贝(SHALLOW COPY)与深拷贝(DEEP COPY)

812 阅读3分钟

在开始说深浅拷贝之前,需要提一下基础知识:

JavaScript 中的对象,只是对内存中某个地址的引用。这些引用是可以变的,即可以重新分配的。因此,仅复制一个引用,会导致 2 个对象指向同一个引用地址,当此引用地址的内容被修改时,所有指向此地址的都会被改变。

让我们看下一个代码示例:

var foo = { a: 'hello' }
console.log(foo.a) // 'hello'

var bar =  foo
console.log(bar.a) // 'hello'

foo.a = 'world'
console.log(foo.a) // 'world'
console.log(bar.a) // 'world'

bar.a = 'hello world'
console.log(foo.a) // 'hello world'
console.log(bar.a) // 'hello world'

可以看到,foobar 指向同一个内存地址,当其中一个被修改时,另一个也会受影响。

浅拷贝 SHALLOW COPY

现在,我们来用一下浅拷贝, ... 展开运算符 和 Object.assign() 都可以实现浅拷贝。

var obj = { foo: 'foo', bar: 'bar' }

var copy = { ...obj }

console.log(obj) // {foo: "foo", bar: "bar"}
console.log(copy) // {foo: "foo", bar: "bar"}

obj.foo = 'abc'
console.log(obj) // {foo: "abc", bar: "bar"}
console.log(copy) // {foo: "foo", bar: "bar"}

OR

var obj = { foo: 'foo', bar: 'bar' }

var copy =  Object.assign({}, obj)

console.log(obj) // {foo: "foo", bar: "bar"}
console.log(copy) // {foo: "foo", bar: "bar"}

obj.foo = 'abc'
console.log(obj) // {foo: "abc", bar: "bar"}
console.log(copy) // {foo: "foo", bar: "bar"}

通过上面两种方法拷贝的对象,修改时并没有相互影响,并且可以把多个源对象拷贝到目标对象。

var obj1 = { foo: 'foo' }
var obj2 = { bar: 'bar' }

var copySpread  =  { ...obj1, ...obj2 }
var copyAssign =  Object.assign({}, obj1, obj2)

上面的方法,看起很爽,然而现实总是有各种办法让你低下高傲的头颅。 当对象的属性就是一个对象的时候,就完蛋了。让我们看下面的代码。

var foo = { a: 'abc', b: { c: '123' } }
var copy = { ...foo }

console.log(foo) // { a: 'abc', b: { c: '123' } }
console.log(copy) // { a: 'abc', b: { c: '123' } }

copy.a = 'edf'
copy.b.c = '456'

console.log(foo) // { a: 'abc', b: { c: '456' } }
console.log(copy) // { a: 'edf', b: { c: '456' } }

copy 修改 copy.b.c 的值时,foo 的值也改变了。。。

深拷贝 DEEP COPY

通过上面部分的代码,相信我们已经理解了浅拷贝,并知道了弊端。那么,就让我们祭出 深拷贝 吧!

第一种解决方案:通过JSON.stringify 将对象序列化为字符串,再通过 JSON.parse 反序列化它。

var foo = { a: 'abc', b: { c: '123' } }
var copy = JSON.parse(JSON.stringify(obj))

是的,没有错,你没有看错,就是这么简单。是不是准备重新抬起你高傲的头颅了? 然而现实还是一如既往的残酷,总是有各种办法让你低下高傲的头颅。

当对象的属性有循环引用时,有不可序列化的值类型时,例如是个 Date对象,JSON.parse也只能将其解释为字符串,而不是Date对象。

那么,让我们把这些情况都考虑进去吧,写一个函数。

function deepCopy(obj) {
  var copy;

  if(obj == null || typeof obj != 'object') return obj

  if(obj instanceof Date) {
    copy = new Date()
    copy = setTime(obj.getTime())

    return copy
  }

  if(obj instanceof Array) {
    copy = []

    for(var i = 0, let = obj.length; i < len; i++) {
      // 多维数组,递归拷贝
      copy[i] = deepCopy(obj[i])
    }

    return copy
  }

  if(obj instanceof Function) {
    copy = function() {
      return obj.apply(this, arguments)
    }

    return copy
  }

  if(obj instanceof Object) {
    copy = {}

    for(var attr in obj) {
      if(hasOwnProperty(attr)) {
        copy[attr] = deepCopy(obj[attr])
      }
    }
    return copy
  }

  throw new Error(`Unable to copy ${obj.constructor.name} as type isn't supported `)
}

总之,在 JavaScript 中复制对象,要考虑到对象的上下文和类型。

但是现实开发中,老板的要求是 用最少的时间,做最多的事,而不是用最多的时间,做一件精品

所以,Lodash 你值得拥有。

Node.js 的深拷贝

从8.0.0版开始,Node.js提供了与结构化克隆兼容的序列化api。

const v8 = require('v8');
const buf = v8.serialize({a: 'foo', b: new Date()});
const cloned = v8.deserialize(buf);
cloned.b.getMonth();