添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
专注前端开发、全栈开发工程狮
博客首页 / 实现深拷贝,支持数组、对象、循环引用 /
实现深拷贝,支持数组、对象、循环引用
2020-08-04 434
什么是深拷贝,这是一个程序员老生常谈的问题了!在工作和面试中,经常遇到对象深拷贝的问题。如果没有真正了解其中的原理,就很容易踩坑了!掌握深拷贝的这个基础知识很重要,无论你是前端写“react”或“vue”,为以后的工作出现深拷贝的问题,就迎刃而解了。

P01:什么是深拷贝

先不说“什么是深拷贝”的文字描述和概念。需要先理解清除一些基础知识点 栈堆 基本数据类型 引用数据类型 ,因为这些概念能更好的让你理解深拷贝与浅拷贝。

基本类型

基本类型都包括哪些呢?有六种: number string boolean null undefined symbol

引用类型

对象 {a:1}, 数组 [1,2,3],以及 函数f unction等

堆栈

而这两类数据存储分别是这样的:

a.基本类型--名值存储在栈内存中 ,例如let a=1;

当你b=a复制时,栈内存会新开辟一个内存,例如这样:

所以当你此时修改a=2,对b并不会造成影响,因为此时的b已自食其力,翅膀硬了,不受a的影响了。当然,let a=1,b=a;虽然b不受a影响,但这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object类型数据。

b.引用数据类型--名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值 ,我们以上面浅拷贝的例子画个图:

当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。

而当我们 a[0]=1 时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。

那,要是在堆内存中也开辟一个新的内存专门为b存放值,就像基本类型那样,岂不就达到深拷贝的效果了

通过了上面的分析,我们清楚了基本原理,明确一下深拷贝和浅拷贝的定义

浅拷贝:

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

深拷贝:

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

话不多说,浅拷贝就不再说了,下面我们直入正题:深拷贝

P02:常用的方法

在不使用第三方库的情况下,我们想要深拷贝一个对象,用的最多的就是下面这个方法。

JSON.parse(JSON.stringify());

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

缺点:

  • 会忽略 undefined
  • 不能序列化函数
  • 不能解决循环引用的对象

显然,面试时你只说出这样的方法是一定不会合格的。

接下来,我们一起来手动实现一个深拷贝方法。

P03:基础版(考虑对象、数组)

解决:忽略undefined、序列化函数问题,考虑兼容对象和数组。

function deepClone(target) {
if (typeof target === 'object') {
var cloneTarget = Array.isArray(target) ? [] : {}
for (const key in target) {
cloneTarget[key] = deepClone(target[key ])
}
return cloneTarget;
} else {
return target;
}
}
var obj = {
name: 'guojun',
age: '30',
sex: undefined,
say: function () {
console.log(this.name)
},
color: ['red', 'blue', 'green']
}
var o = deepClone(obj)

P04:循环引用

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

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

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function deepClone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = deepClone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
}
var obj = {
name: 'guojun',
age: '30',
sex: undefined,
say: function () {
console.log(this.name)
},
color: ['red', 'blue', 'green']
}
obj.obj = obj;
var o = deepClone(obj)

P05:最终版(其他数据类型)

考虑兼容一些其他的数据类型。代码如下:

// 完美解决方案
function deepClone(target, map = new WeakMap()) {
if (typeof target === 'object') {
var type = Object.prototype.toString.call(target)
if (type === '[object Number]' || type === '[object String]' || type === '[object Boolean]' || type === '[object Error]' || type === '[object Date]') {
return new target.constructor
} else if (type === '[object Symbol]') {
return Object(Symbol.prototype.valueOf.call(target))
} else {
const cloneTarget = Array.isArray(target) ? [] : {}
if (map.get(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
for (let key in target) {
cloneTarget[key] = deepClone(target[key], map)
}
return cloneTarget
}
} else {
return target
}
}
var obj = {
name: 'guojun',
say: () => {
console.log(this.name)
},
color: ['red', 'blue'],
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
error: new Error(),
children: {
name: 'guozhixuan',
age: 8,
}
}
var o = deepClone(obj)

最后这个一般我们的实际工作中很少会遇到对象有特殊类型的情况。只要掌握一种就基本够用了!