一个简单的深拷贝

900 阅读5分钟

前言

最近看到一篇关于深拷贝和浅拷贝的文章,分析得特别详细,看完觉得学到了很多,因此想做个‘笔记’,希望对大家也能有所帮助。

原文:juejin.cn/post/684490…

深拷贝和浅拷贝的含义

浅拷贝: 创建一个拷贝对象,把当前对象的所有属性都拷贝过来。如果属性是基本类型,那拷贝的就是基本类型的值;如果属性是引用类型,那拷贝的就是它的内存地址。这两个对象中任何一个对象的引用类型的属性发生了变化,都会影响另一个对象。 1663429002598-37af07fe-6968-42cf-81e3-d667b1f95bf3.png

深拷贝: 创建一个拷贝对象,把当前对象的所有值都拷贝过来。如果是引用类型,那拷贝的也是它的值,而不是内存地址。 1663429022748-41c61de3-099c-4993-8842-2992ec96e2cf.png

浅拷贝的实现方式

以数组类型为例,简单的浅拷贝可以这样写:

    function shallowCopy(target){
        let copyTarget = [];
        for(const index in target){
            copyTarget[index] = target[index];
        }
        return copyTarget;
    }

简单的深拷贝

如果数据是可以用json格式来表示的,比如:String、Number、Boolean、Array等,那我们可以用以下的方式来实现:

JSON.parse(JSON.stringfy());

但是这样做有缺陷,因为在序列化JavaScript对象的时候,所有的函数和原型成员会被忽略。

进阶版的深拷贝

二话不说,我先放个代码,以下不是深拷贝最全面的实现方式,需要考虑到的方面还有很多,其他细节可以在分享的原文中进行了解。

const target = {
  field1: 1,
  field2: undefined,
  field3: 'ConardLi',
  field4: {
      child: 'child',
      child2: {
          child2: 'child2'
      }
  },
  field5:[10,20,30]
};

//判断是否为引用类型
function isObject(target){
  const type = typeof target;
  return target !== null && (type === 'object' || type === 'function');
}

//封装一个循环方法
function myWhile(array, fn){
  let index = -1;
  const length = array.length;
  while(++index < length){
    fn(array[index], index);
  }
}

function deepCopy(target, map = new Map()){
  if(!isObject(target)){
    return target;
  }else{
    const isArray = Array.isArray(target);
    let copyTarget = isArray ? []:{};
    if(map.get(target)){
      return map.get(target);
    }
    map.set(target, copyTarget);
    const keys = isArray ? undefined : Object.keys(target);
    myWhile(keys || target, (value,index)=>{
      if(keys){
        index = value;
      }
      copyTarget[index] = deepCopy(target[index], map);
    });
    return copyTarget;
  }
}

const copyResult = deepCopy(target);
console.log('copyResult',copyResult);

接下来,我分段解释一下,为什么这么写:

数据类型

首先,JavaScript中有两种数据类型:基本数据类型和引用数据类型。 基本数据类型有7种:

  • String 字符串
  • Number 数值
  • Boolean 布尔值
  • Null 空值
  • Undefined 未定义
  • Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
  • BigInt 可以表示任意大小的整数

其中Symbol和BigInt是ES6中新增的数据类型。

引用数据类型有:Object类型、Array类型、Date类型、RegExp类型、Function类型、内置对象(Math对象、Global对象)等。

在JavaScript中,一切皆对象,也就是说所有的对象都继承自Object,都有toString方法,但是,考虑到像Date和Array这样的数据类型一般会重写toString方法,所以用Object原型上的toString.call()方法可以检测所有的数据类型。

Object.prototype.toString.call();

当我们用typeof来检测对象的类型时,如果是null、Array、Object和其他的引用类型,我们得到的返回值是‘object’。

//判断是否为引用类型
function isObject(target){
  const type = typeof target;
  return target !== null && (type === 'object' || type === 'function');
}

//deepCopy中:
if(!isObject(target)){
   return target;
}

在深拷贝方法中,如果是基本数据类型,我们直接返回它的值,如果是其他的引用类型,我们另做处理。所以如果当前对象的值是null就直接return,但是用typeof去监测null得到的是'object',所以在isObject方法中需要特殊处理null,而用typeof去检测Function类型得到的是'function',所以也需要特殊处理。

循环引用

循环引用:即对象的属性直接或间接的引用了自身。

1663318637091-39c92d80-c339-4fcc-ac24-168bd1d068e0.png

解决循环引用的问题:

我们额外开辟一个存储空间,用key-value的形式来存储当前对象和拷贝对象之间的对应关系,当需要拷贝一个对象的时候,先去存储空间里面看,有没有拷贝过这个对象,有就直接返回,没有就继续拷贝,并且把当前对象作为key,拷贝对象作为value存储进去。

if(map.get(target)){
   return map.get(target);
}
map.set(target, copyTarget);

封装while

原文中表示用for in循环进行遍历效率低,耗时高,用while循环会更快,所以封装一个while循环。当然我自测的话,是差不多的。

//封装一个循环方法
function myWhile(array, fn){
  let index = -1;
  const length = array.length;
  while(++index < length){
    fn(array[index], index);
  }
}

//deepCopy中:
const isArray = Array.isArray(target);
const keys = isArray ? undefined : Object.keys(target);
myWhile(keys || target, (value,index)=>{
   if(keys){
      index = value;
   }
   copyTarget[index] = deepCopy(target[index], map);
});

因为循环遍历的主体是一个数组,如果要拷贝的当前对象是一个object类型,那它的length值就是undefined,不能进入while循环,我们可以用Object.keys()方法获取到当前对象的所有的key,它的返回值是一个数组。

当遍历数组时,直接使用myWhile进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把myWhile回调函数的value当作key使用。

copyTarget[index] = deepCopy(target[index], map);

考虑到当前对象里面不知道有多少层,所以我们在遍历的时候,把当前对象上的每一个属性都以递归的方式经过深拷贝之后再添加到拷贝对象上。

最后

对于不同的数据类型需要不同的处理方式,我们还可以划分各种类型来单独处理,但是这边就不多做介绍了,最主要的是要了解深拷贝的真正意义,日常开发中,我们或许用不到那么复杂的深拷贝,大家根据实际情况设计即可。