在前端开发中我们经常会遇到拷贝的问题,这也是一个面试经常会被问到的问题。 我们都知道有深拷贝和浅拷贝,那我们来聊聊它们的
形成原因和常见的深浅拷贝方法。
一、深浅拷贝的原因
- JS数据类型分为
8种数据类型:'Number'、'String'、'Boolean'、'Null'、'Undefined'、
'Object'、'Symbol'、'BigInt'
基本类型:String、Number、Boolean、Null、Undefined、Symbol、BigInt
引用类型:Object
- JS数据存储
不同类型数据存在不同的内存中
栈内存:存放基本数据类型和引用数据类型的地址(也可理解为指针)
堆内存:存放引用数据类型的值也就是对象
- 深浅拷贝
深浅拷贝一般都是相对于'引用数据类型'来进行探讨的;
当然对于'基本数据类型'来说,拷贝也都可理解为深拷贝(完全复制)
浅拷贝:将原来栈内存中存储的地址(指针)进行了一份复制存到新的栈内存地址,所指向的堆内存数据还是原来的数据
深拷贝:将原来的内存的数据(栈内存和堆内存)都进行了一份复制存到新的地址中,新的栈内存中的新地址指向的是新的堆内存中的数据
二、深浅拷贝的方法
1、浅拷贝方法
浅拷贝一般就是栈内存数据copy一份存到新的栈内存中,比如:
对于普通类型:
let a=1;
let b=a;
console.log(a,b);// 1,1
b=2;
console.log(a,b);// 1,2
// 可以看到b的改变,并未影响a的值,即两个是独立的两个栈内存,互不影响
// 另外注意一点,基本数据类型不能增加新的属性,
b.c=3;
console.log(b) // undefined
对于引用类型:
let obj={a:1,b:2};
let objCopy = obj;
console.log(obj,objCopy); //{a: 1, b: 2} {a: 1, b: 2}
objCopy.b=3;
objCopy.c=4;
console.log(obj,objCopy); //{a: 1, b: 3, c: 4} {a: 1, b: 3, c: 4}
// 可以看到对于引用类型的数据,copy以后的改动还是对原堆内存中数据的修改
2、深拷贝方法
展开运算符...
Object.assign
let obj = { a: 1, b: 2};
let objCopy = Object.assign({},obj)
let objCopy2 = {...obj}
console.log('obj:', obj) //{a: 1, b: 2}
console.log('objCopy:', objCopy) //{a: 1, b: 2}
console.log('objCopy2:', objCopy2) //{a: 1, b: 2}
objCopy.c = 4
objCopy2.c = 5
console.log('obj:', obj) //{a: 1, b: 2}
console.log('objCopy:', objCopy) //{a: 1, b: 2,c: 4}
console.log('objCopy2:', objCopy2) //{a: 1, b: 2,c: 5}
以上代码看起来貌似没什么问题,然而,当obj的key对应的value是Object类型时,就会出现问题。
//以 Object.assign 为例,展开运算符是一样的结果
let obj = { a: 1, b: 2, c: { c1: 0 } };
let objCopy = Object.assign({},obj);
console.log('obj:', obj) //{a: 1, b: 2, c: {c1: 0}}
console.log('objCopy:', objCopy) //{a: 1, b: 2, c: {c1: 0}}
objCopy.c.c1 = 4
console.log('obj:', obj) //{a: 1, b: 2, c: {c1: 4}}
console.log('objCopy:', objCopy) //{a: 1, b: 2, c: {c1: 4}}
// 可以看出当key对应的value为Object时,改动objCopy 仍然会影响 obj
那么这两种拷贝方法就只能局限在对象的key对应的value 只能是
原始类型
JSON.parse与JSON.stringify
通过JSON.stringify(Object) 转换为String类型,进行基本类型的拷贝; 再通过JSON.parse() 将经过转换的特殊String再转为Object。
这是我们经常用的深拷贝的方法,举例:
let obj = { a: 1, b: 2, c: { c1: 3 } };
let objCopy = JSON.parse(JSON.stringify(obj))
console.log('obj:', obj) //{ a: 1, b: 2, c: { c1: 3 } }
console.log('objCopy:', objCopy) //{ a: 1, b: 2, c: { c1: 3 } }
objCopy.c.c1 = 4
console.log('obj:', obj) //{ a: 1, b: 2, c: { c1: 3 } }
console.log('objCopy:', objCopy) //{ a: 1, b: 2, c: { c1: 4 } }
//可以解决展开运算符...与Object.assign()的缺陷,对多层嵌套也可以很好的拷贝
然而,当obj的key对应的value是Object某些特殊类型时,如Date对象、RegExp对象、Function函数时,会发现这种方法并不能完美的copy
1. key对应的value为 Function
let obj = { a: 1, b: 2, c: function () { console.log(111) } };
let objCopy = JSON.parse(JSON.stringify(obj))
//输出objCopy为 {a: 1, b: 2} objCopy丢弃了c:function(){}
2. key对应的value为 RegExp
let obj = { a: 1, b: 2, c:RegExp("123") };
let objCopy = JSON.parse(JSON.stringify(obj))
//输出objCopy为 {a: 1, b: 2, c: {}} objCopy.c的value被转换为{}
3.
let obj = { a: 1, b: 2, c:new Date() };
let objCopy = JSON.parse(JSON.stringify(obj))
//输出objCopy为 {a: 1, b: 2, c: "2020-07-06T11:21:11.234Z"} objCopy.c的value被转换为 日期字符串
4. 还有其他的类型值,可自行实验一下
参考`stackoverflow`:
you do not use Dates, functions, undefined, Infinity, RegExps, Maps, Sets,
Blobs, FileLists, ImageDatas, sparse Arrays,Typed Arrays or other
complex types within your object,
a very simple one liner to deep clone an object is.
5. 当对obj循环引用时
let obj = { a: 1, b: 2 };
obj.c=obj;
let objCopy = JSON.parse(JSON.stringify(obj))
报错:Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'c' closes the circle
at JSON.stringify (<anonymous>)
受限于JSON.stringify() 可转换的数据必须是非infinite层级结构的数据
通过以上的例子可以明显看出,JSON转换的形式仅适用于符合json数据格式的数据结构;其限制也比较多,但相比于前几种形式优点也不言而喻,只要能确定数据符合要求,即可放心适用这种方法
递归
递归方法众多,可自行实现;这里不再实现。
考虑一个问题:如果是"循环引用的对象",必然会造成死循环。
怎么能解决这个问题?(thinking...)
已经存在的轮子,loadsh的cloneDeep()
如何使用可以查看Loadsh官网