深拷贝是什么
在JS中,所有的拷贝API都是浅拷贝,比如数组的拷贝,我们一般使用Array.prototype.slice来拷贝一个数组,但是对于嵌套数组,就会只拷贝其中的引用。
const arr=[[1,2,3],[4,5,6]]
const arr2=arr.slice()
arr2[0].push(666)
console.log(arr) //[[1, 2, 3, 666], [4, 5, 6]]
上面的arr2是拷贝后的数组,arr2改变了同样会影响到arr的值,所以这就不是深拷贝。
官方解释是这样的
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
我所理解的深拷贝,就是当我克隆一个东西出来之后,跟原来的完全不相交。
序列化与反序列化
在工作中,我们一般都会使用序列化进行深拷贝,就是采用JSON.stringify和JSON.parse来进行深拷贝
const obj={
name:'yanxi',
props:{name:'qiu'}
}
const obj2=JSON.parse(JSON.stringify(obj))
obj2.props.name='11111'
obj
//{name: "yanxi", props: {name: "qiu"}}
这种方式非常简单,但JSON的局限性较大。
局限性如下:
1、不支持函数,会自动忽略
2、不支持undefined,JSON天然不支持
3、不支持环状引用(即引用自身)会报错
4、不支持Date,会转成字符串
5、不支持symbol,JSON天然不支持
我们可以看到JSON.parse(JSON.stringify())虽然能够深拷贝一个对象,但是存在很大的局限性,对于复杂的对象就不适用了。因此,我们需要采用另外的方式来实现深拷贝,也就是通过递归的方式手动实现深拷贝。
递归深拷贝
JS中存在七种类型:number、string、boolean、symbol、null、undefined、object
其中除了object属于引用类型,其余都是简单数据类型,所以我们主要要对object的数据类型进行分辨,其余的简单数据类型都只要直接返回即可。
简单数据类型深拷贝
简单数据是不可变的
let a=1
let b=a
b=2
a //1
上面的代码只是改变了b的指向,并不影响原来的数据a。
那么我们就可以直接实现对于简单数据类型的深拷贝函数
function deepClone(target){
return target
}
普通对象深拷贝
const obj={
name:`qiuyanxi`,
fullMessage:{
firstName:'qiu',
age:20,
}
}
上面就是一个普通对象,我们通过检测其类型,然后创建一个新对象,对源数据循环后递归来返回源数据的具体属性
只要对象有尽头,最终的具体数据肯定是简单数据类型,那么只要最终找到底层数据,然后拷贝出去就行了。这也是递归深拷贝的核心思想。
function deepClone(target){
if(target instanceof Object){//普通对象深拷贝
const newObj=Object.create(null)
for(let k in target){
newObj[k]=deepClone(target[k])
}
return newObj
}
return target
}
上面的代码创建了一个新的对象,然后通过递归把每个对象上的属性拷贝到新对象上。
如果这个属性是简单类型的那么就直接返回这个属性值。如果是Object类型,那么就通过for...in遍历讲对象上的每个属性一个一个地添加到新的对象身上。因为无法区分对象的层级,因此使用递归,每次赋值时都是调用自己,反正如果是简单类型就递归一次直接返回值,如果是Object类型,那么就往下递归查找赋值。
一般的递归是要设置终止条件的,但是对象因为自身存在终止条件,即到达其原型链最顶层会自动停止,所以我们不需要再设置终止条件。
数组的深拷贝
数组可以使用for of循环
function deepClone(target){
if(target instanceof Object){//普通对象深拷贝
if(target instanceof Array){//数组深拷贝
const newArr=[]
for(let v of target){
newArr.push(deepClone(v))
}
return newArr
}
const newObj=Object.create(null)
for(let k in target){
newObj[k]=deepClone(target[k])
}
return newObj
}
return target
}
函数深拷贝
因为在很多人看来函数是无法拷贝的。在我看来函数实际上不应该有深拷贝的,如果真的要有,那么也就是实现函数的功能。
应该如何实现一个函数的拷贝了?
- 需要返回一个新的函数
- 新的函数执行结果必须与原函数相同。
function deepClone(target){
if(target instanceof Object){//普通对象深拷贝
if(target instanceof Array){//数组深拷贝
const newArr=[]
for(let v of target){
newArr.push(deepClone(v))
}
return newArr
}else if(target instanceof Function){//函数深拷贝
return (...args)=>{
return target.call(this,...args)
}
}
const newObj=Object.create(null)
for(let k in target){
newObj[k]=deepClone(target[k])
}
return newObj
}
return target
}
正则表达式
正则表达式的形式是这样的/[0-9]{1,2}/ig,我们可以通过两个正则的属性获取到
/[0-9]{1,2}/ig.source //"[0-9]{1,2}"
/[0-9]{1,2}/ig.flags //gi
我们深拷贝一个正则实际上就是拿到这两部分,然后重新创建一个新的正则,从而实现跟原来的正则相同的功能即可.
function deepClone(target){
if(target instanceof Object){
if(target instanceof Array){//数组深拷贝
const newArr=[]
for(let v of target){
newArr.push(deepClone(v))
}
return newArr
}else if(target instanceof Function){//函数深拷贝
return (...args)=>{
return target.call(this,...args)
}
}else if(target instanceof RegExp){ //深拷贝正则
return new RegExp(target.source,target.flags)
}
const newObj=Object.create(null)//普通对象深拷贝
for(let k in target){
newObj[k]=deepClone(target[k])
}
return newObj
}
return target
}
日期深拷贝
日期的拷贝跟正则表达式一样,假设我们需要拷贝一个日期,可以使用new Date构造函数传入原来的日期,就可以完成深拷贝日期操作
function deepClone(target){
if(target instanceof Object){
if(target instanceof Array){//数组深拷贝
const newArr=[]
for(let v of target){
newArr.push(deepClone(v))
}
return newArr
}else if(target instanceof Function){//函数深拷贝
return (...args)=>{
return target.call(this,...args)
}
}else if(target instanceof RegExp){ //深拷贝正则
return new RegExp(target.source,target.flags)
}else if(target instanceof Date){
return new Date(target) //深拷贝日期
}
const newObj=Object.create(null)//普通对象深拷贝
for(let k in target){
newObj[k]=deepClone(target[k])
}
return newObj
}
return target
}
优化后的代码
function deepClone(target) {
if (target instanceof Object) {
let result = {}; //普通对象深拷贝
if (target instanceof Array) {
result = []; //数组深拷贝
} else if (target instanceof Function) {
//函数深拷贝
result = (...args) => {
return target.call(this, ...args);
};
} else if (target instanceof RegExp) {
//深拷贝正则
result = new RegExp(target.source, target.flags);
} else if (target instanceof Date) {
result = new Date(target); //深拷贝日期
}
Object.keys(target).forEach((k)=>{
result[k]=deepClone(target(k))
})//使用这个代替for in
return result;
}
return target;
}
由于for in会循环出原型链上的属性,所以在这里我改了成 Object.keys(target)和forEach循环来遍历对象。