Skip to content

Latest commit

 

History

History
executable file
·
407 lines (303 loc) · 13.6 KB

深拷贝与浅拷贝.md

File metadata and controls

executable file
·
407 lines (303 loc) · 13.6 KB

前言

在 javascript 中有不同的方法来拷贝对象,如果你还不熟悉这门语言的话,拷贝对象时就会很容易掉进陷阱里,那么我们怎样才能正确地拷贝一个对象呢?

读完本文,希望你能明白:

  • 什么是深/浅拷贝,他们跟赋值有何区别?
  • 深/浅拷贝的实现方式有几种?

赋值和深/浅拷贝的区别

这三者的区别如下,不过比较的前提都是针对引用类型

变量赋值

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,改变的都是存储空间的内容,因此两个对象是联动的。

如下所示:

let obj1 = {
    name : 'sunshine',
    arr : [1, [2, 3] ,4],
}
let obj2 = obj1
obj2.name = 'colorful'
obj2.arr[1] = [5, 6, 7]
console.log(obj1) // { name: 'colorful', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log(obj2) // { name: 'colorful', arr: [ 1, [ 5, 6, 7 ], 4 ] }

浅拷贝

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

特点:浅拷贝是重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

下面将 obj2 的赋值方式改为浅拷贝:

let obj1 = {
    name: 'sunshine',
    arr : [1, [2, 3] ,4],
}
let obj2 = Object.assign({}, obj1)
obj2.name = 'colorful'
obj2.arr[1] = [5, 6, 7] // 新旧对象还是共享同一块内存
console.log(obj1) // { name: 'sunshine', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log(obj2) //{ name: 'colorful', arr: [ 1, [ 5, 6, 7 ], 4 ] }

深拷贝

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

特点:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

下面将 obj2 的赋值方式改为深拷贝:

let obj1 = {
    name: 'sunshine',
    arr : [1, [2, 3] ,4],
}
let obj2 = JSON.parse(JSON.stringify(obj1))
obj2.name = 'colorful'
obj2.arr[1] = [5, 6, 7] // 新旧对象还是共享同一块内存
console.log(obj1) // { name: 'sunshine', arr: [ 1, [ 2, 3 ], 4 ] }
console.log(obj2) // { name: 'colorful', arr: [ 1, [ 5, 6, 7 ], 4 ] }

小结

不同方式对原始数据的影响

方式 和原数据指向同一对象 第一层基本类型数据 原数据中包含子对象
变量赋值 改变后修改原数据 改变后修改原数据
浅拷贝 改变后是不会修改原数据 改变后修改原数据
深拷贝 改变后不会修改原数据 改变后不会修改原数据

浅拷贝的实现方式

1. for...in

function clone (target) {
    let cloneTarget = {}
    for (const key in target) {
        cloneTarget[key] = target[key]
    }
    return cloneTarget
}

2. Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。它的原理是:用合并对象的方式来覆盖前面的对象,并改变第一个对象的值

Object.assign() 可以避免对象指针覆盖导致的数据污染问题。即浅拷贝,如下所示:

function func (obj) {
  const funcObj = Object.assign({}, obj)
  funcObj.name = 'func obj'
  funcObj.person.age = 30
}
  
const obj = { name: 'global obj', person: { age: 20 } }
  
func(obj)
console.log(obj) // { name: 'global obj', person: { age: 30 } }

3. 展开运算符...

展开运算符是一个 ES6 / ES2015 特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign() 的功能相同。

let obj1 = { name: 'sunshine', person: { age: 20 } }
let obj2= { ...obj1 }
obj2.person.age = 30
obj2.name = 'colorful'
console.log(obj1) // { name: 'sunshine', person: { age: 30 } }

4. Array.prototype.concat()

let arr = [1, 3, { username: 'sunshine' }]
let arr2 = arr.concat()  
arr2[0] = 2
arr2[2].username = 'colorful'
console.log(arr) // [ 1, 3, { username: 'colorful' } ]

5.Array.prototype.slice()

let arr = [1, 3, { username: 'sunshine' }]
let arr2 = arr.slice()
arr2[0] = 2
arr2[2].username = 'colorful'
console.log(arr) // [ 1, 3, { username: 'colorful' } ]

6.第三方库API

lodash中的_.clone(obj)

underscore中的_.clone(obj)

第三方库中的数据拷贝,重新创建了一个新的数据地址,并且根据不同的浏览器兼容性使用的方式也不一样。

使用方式类似:

var _ = require('lodash')
var obj1 = {
    a: 1,
    b: { f: 2 }
}
var obj2 = _.clone(obj1)
obj2.a = 2
obj2.b.f = 3
console.log(obj1) // { a: 1, b: { f: 3 }}

小结

  • 对象浅拷贝用 Object.assign() 或展开运算符...
  • 数组浅拷贝用 slice() 或 concat() 方法
  • 如果对浏览器兼容性有要求,建议使用第三方库提供的API
  • 对于复杂数据结构的对象,建议使用深拷贝

深拷贝的实现方式

1. JSON.parse(JSON.stringify())

let arr = [1, 3, { username: 'sunshine' }]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[0] = 2
arr2[2].username = 'colorful'
console.log(arr) // [ 1, 3, { username: 'sunshine' } ]

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但是不能处理函数和正则等一些特殊的数据类型,因为这些数据基于 JSON.stringify 和 JSON.parse 处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

let arr = [1, 3, { username: 'sunshine' }, function(){} ]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].username = 'colorful'
console.log(arr, arr2)

2.第三方库API

jquery.extend()

jquery 有提供一個$.extend可以用来做 Deep Copy, $.extend(deepCopy, target, object1, [objectN]) ,第一个参数为true,就是深拷贝

使用方式:

var $ = require('jquery')
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
}
var obj2 = $.extend(true, {}, obj1)
console.log(obj1.b.f === obj2.b.f) // false

lodash 中的 _.cloneDeep(obj)

var _ = require('lodash')
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f) // false

3.手写递归方法

递归方法实现深拷贝原理:遍历对象直到里边都是基本数据类型,然后再去拷贝,就是深度拷贝

基本实现

对象类型有很多种,最常用的当属Object和Array,下面简单实现以下常见类型的数据深拷贝:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象或数组,遍历需要拷贝的对象,将需要拷贝对象的属性执行深拷贝后依次添加到新对象上
function cloneDeep(target) {
  if (typeof target !== 'object') return target
  let cloneTarget = Array.isArray(target) ? [] : {}
  for (const key in target) {
    cloneTarget[key] = cloneDeep(target[key])
  }
  return cloneTarget
}

// 测试
let obj1 = { name: 'sunshine', person: { age: 20 }, arr: [1, 2, 3] }
let obj2 = cloneDeep(obj1)
obj2.name = 'colorful'
obj2.person.age = 30
obj2.arr[0] = 4
console.log(obj1) // { name: 'sunshine', person: { age: 20 }, arr: [1, 2, 3] }
console.log(obj2) // { name: 'colorful', person: { age: 30 }, arr: [4, 2, 3] }

循环引用

如果对象的属性直接的引用了自身,即对象循环引用,就会因为递归进入死循环导致栈内存溢出了。

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  1. 检查map中有无拷贝过的对象
  2. 有 - 直接返回
  3. 没有 - 将当前对象作为key,拷贝对象作为value进行存储
  4. 继续拷贝
function cloneDeep(target, map  = new Map()) {
  if (typeof target !== 'object') return target
  let cloneTarget = Array.isArray(target) ? [] : {}
  // 解决循环引用问题
  if (map.get(target)) {
    return map.get(target)
  }
  map.set(target, cloneTarget)
  
  for (const key in target) {
    cloneTarget[key] = cloneDeep(target[key], map)
  }
  return cloneTarget
}

// 测试
let obj1 = { name: 'sunshine', person: { age: 20 }, arr: [1, 2, 3] }
obj1.o = obj1
let obj2 = cloneDeep(obj1)
console.log(obj1) 
// { name: 'sunshine', person: { age: 20 }, arr: [1, 2, 3], o:{...obj1本身} }

性能优化-减少内存消耗

接下来,我们可以使用,WeakMap提代Map来使代码达到画龙点睛的作用。

function cloneDeep(target, map = new WeakMap()) {
    // ...
}

为什么要这样做呢?先来看看WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

性能优化-提升执行效率

我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,在反复测试后得出的执行效率的大小排序是:while > for in > for

所以,我们可以想办法把for in遍历改变为while遍历

function cloneDeep(target, map  = new WeakMap()) {
  if (typeof target !== 'object') return target
	// 判断对象是否为数组
  const isArray = Array.isArray(target);
  let cloneTarget = isArray ? [] : {};

  if (map.get(target)) {
    return map.get(target)
  }
  map.set(target, cloneTarget)
  
  // 将for in 改为 while
  // for (const key in target) {
  //   cloneTarget[key] = cloneDeep(target[key], map)
  // }
  // 判断数组或对象是否为空,为空不做递归
  const keys = isArray ? undefined : Object.keys(target)
  whileFn(keys || target, (value, key) => {
    if (keys) key = value
    cloneTarget[key] = cloneDeep(target[key], map);
  })
  return cloneTarget
}

function whileFn(array, callback) {
  let index = -1
  const length = array.length
  while (++index < length) {
    callback(array[index], index)
  }
  return array
}

// 测试
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8],
    f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
}

target.target = target

// 可以分别测试改写前后拷贝的时间
console.time()
const result = cloneDeep(target)
console.timeEnd()

其它数据类型

在上面的代码中,我们其实只考虑了普通的objectarray两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我就不一一尝试了,最后附赠全部代码:https://github.com/ConardLi/ConardLi.github.io/blob/master/demo/deepClone/src/clone_6.js

小结

希望看完本篇文章能对你有如下帮助:

  • 理解深浅拷贝的真正意义
  • 能整我深拷贝的各个要点,对问题进行深入分析
  • 可以手写一个比较完整的深拷贝

文中如有错误,欢迎在评论区指正。

参考文章

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可我的微信公众号【阳姐讲前端】,每天推送高质量文章,我们一起交流成长。