手写深拷贝

简介: 手写深拷贝

偶然发现自己欠了一篇文章,那么今天就来自己动手实现一个深拷贝


之前的文章(《赋值、浅拷贝与深拷贝》)我们讲过深浅拷贝的概念、区别以及JSON.parse(JSON.stringify(obj))实现深拷贝存在的问题:


对于undefined,函数,Symbol会直接忽略


new Date()转换后结果不正确


对于正则转换为{}


对于循环引用,会报错


说到底,想要实现深拷贝,就是浅拷贝加递归,也就是如果对象的属性值还是一个对象的话,再进行一次拷贝,话不多说,上代码:


首先定义一个供深拷贝使用的对象:


let obj1 = {
  name:'obj.name',
  un:undefined,
  nu:null,
  sy:Symbol(123),
  say:function(){
    console.log(this.name);
  },
  reg:/\d{6}/g,
  date:new Date(),
  child:{
    name:'child.name'
  }
}复制代码


可见如上对象的属性值包含了JSON.parse(JSON.stringify(obj))存在问题的所有数据类型,接下来让我们实现一个深拷贝并一一解决JSON.parse(JSON.stringify(obj))深拷贝存在的问题


首先我们讲,实现深拷贝,就是遍历对象的key,并将value赋给新的对象的key,如果原对象的属性值为对象,则递归调用深拷贝方法(这里指的属性值为对象指有自己属性的对象,区别于正则,Date对象等),于是有了如下第一版代码:


function isObject(obj) {
  return typeof obj === 'object' && obj != null
}
function deepCopy(source){
  // 判断如果参数不是一个对象,返回改参数
  if(!isObject(source)) return source;
  // 判断参数是对象还是数组来初始化返回值
  let res = Array.isArray(source)?[]:{};
  // 循环参数对象的key
  for(let key in source){
    // 如果该key属于参数对象本身
    if(Object.prototype.hasOwnProperty.call(source,key)){
      // 如果该key的value值是对象,递归调用深拷贝方法进行拷贝
      if(isObject(source[key])){
        res[key] = deepCopy(source[key]);
      }else{
        // 如果该key的value值不是对象,则把参数对象key的value值赋给返回值的key
        res[key] = source[key];
      }
    }
  }
  // 返回返回值
  return res;
};复制代码


然后用如下代码来比对该方法的成果:


let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码


查看控制台输出结果:



可见第一版方法对于Date,正则的拷贝变成了空对象,对于方法及Symbol的拷贝都是没有问题的,其实对于第一版方法中判断source[key]是否是对象的方法isObject,对于Date对象和正则也会返回true,而这两种对象再次递归调用深拷贝方法的时候,由于其没有可遍历的key,所以返回的就是初始化的{},找到了问题点,我们优化上面的方法如下:


function isObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'||Object.prototype.toString.call(obj) ==='[object Array]'
}
function deepCopy(source){
  // 判断如果参数不是一个对象,返回改参数
  if(!isObject(source)) return source;
  // 判断参数是对象还是数组来初始化返回值
  let res = Array.isArray(source)?[]:{};
  // 循环参数对象的key
  for(let key in source){
    // 如果该key属于参数对象本身
    if(Object.prototype.hasOwnProperty.call(source,key)){
      // 如果该key的value值是对象,递归调用深拷贝方法进行拷贝
      if(isObject(source[key])){
        res[key] = deepCopy(source[key]);
      }else{
        // 如果该key的value值不是对象,则把参数对象key的value值赋给返回值的key
        res[key] = source[key];
      }
    }
  }
  // 返回返回值
  return res;
};复制代码


再次用如下代码来比对该方法的成果:


let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码


查看控制台输出结果:



可见第二版的方法对于Date和正则的拷贝已经完全没有问题了,那么我们再处理最后一个问题:循环引用


调用深拷贝之前添加如下代码:


obj1.child.child= obj1.child;
复制代码

再次用如下代码来比对该方法的成果:


let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码

查看控制台输出结果:


会发现第二版的方法对于循环引用的对象不停地递归调用,然后就爆栈了


解决如上方法,只需要在递归深拷贝之前判断是否已经拷贝过该对象,是的话把该对象返回,不要再递归下去即可,采用ES6的WeakMap对象保存已经拷贝过的对象,修改

deepCopy方法如下:


function deepCopy(source,hash = new WeakMap()){
  // 判断如果参数不是一个对象,返回改参数
  if(!isObject(source)) return source;
  if(hash.has(source)) return hash.get(source); // 如果拷贝过该对象,则直接返回该对象
  // 判断参数是对象还是数组来初始化返回值
  let res = Array.isArray(source)?[]:{};
  hash.set(source,res); // 哈希表添加新对象
  // 循环参数对象的key
  for(let key in source){
    // 如果该key属于参数对象本身
    if(Object.prototype.hasOwnProperty.call(source,key)){
      // 如果该key的value值是对象,递归调用深拷贝方法进行拷贝
      if(isObject(source[key])){
        res[key] = deepCopy(source[key],hash);
      }else{
        // 如果该key的value值不是对象,则把参数对象key的value值赋给返回值的key
        res[key] = source[key];
      }
    }
  }
  // 返回返回值
  return res;
};
复制代码


再次用如下代码来比对该方法的成果:


let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码


查看控制台输出结果:



可见第三版方法对于对象的循环引用也可以完美拷贝了!


如果有错误或者不严谨的地方,请给予指正,十分感谢!

相关文章
|
存储 缓存 Python
如何使用Python抓取PDF文件并自动下载到本地
如何使用Python抓取PDF文件并自动下载到本地
1095 0
|
5月前
|
机器学习/深度学习 自然语言处理 数据可视化
⼤模型驱动的DeepInsight Copilot在蚂蚁的技术实践
本文整理自潘兰天(蚂蚁数据智能团队数据分析平台技术专家)在DA数智大会2025·上海站的演讲实录。
|
搜索推荐 Android开发 UED
探索安卓开发中的自定义视图:打造个性化用户界面
【7月更文挑战第31天】在安卓应用的海洋中,一个独特且吸引人的用户界面是捕获用户眼球的关键。本文将带你深入理解如何在Android开发中创建自定义视图,从而设计出与众不同的用户界面。我们将一起探索如何从零开始构建一个自定义视图组件,并实现动态交互效果。通过实际的代码示例和详细的步骤解析,你将学会如何提升你的应用界面,让它在众多应用中脱颖而出。
223 31
|
SQL 安全 Java
Web Security 之 Server-side template injection
Web Security 之 Server-side template injection
376 0
|
网络协议 关系型数据库 Linux
PostGresql数据库Linux服务器安装
PostGresql数据库,Linux服务器,在线,离线安装
4383 2
PostGresql数据库Linux服务器安装
|
数据可视化 Java 数据安全/隐私保护
【Spring Cloud Alibaba Sentinel 实现熔断与限流】 —— 每天一点小知识(上)
【Spring Cloud Alibaba Sentinel 实现熔断与限流】 —— 每天一点小知识
463 0
|
数据可视化 前端开发 C#
WPF技术之图形系列Ellipse控件
WPF Ellipse是Windows Presentation Foundation (WPF)中的一个图形控件,它用于绘制椭圆形状。在WPF中,Ellipse可以用于创建具有椭圆形状的可视化效果,可以设置其位置、大小、填充颜色等属性。
1589 0
|
canal 存储 缓存
如何优雅的记录操作日志
操作日志几乎存在于每个系统中,而这些系统都有记录操作日志的一套 API。操作日志和系统日志不一样,操作日志必须要做到简单易懂。所以如何让操作日志不跟业务逻辑耦合,如何让操作日志的内容易于理解,如何让操作日志的接入更加简单?上面这些都是本文要回答的问题。我们主要围绕着如何“优雅”地记录操作日志展开描述,希望对从事相关工作的同学能够有所帮助或者启发。
939 0
如何优雅的记录操作日志
|
XML 开发框架 前端开发
在ASP.Net Core下,Autofac实现自动注入
在ASP.Net Core下,Autofac实现自动注入
342 0
在ASP.Net Core下,Autofac实现自动注入
|
关系型数据库 MySQL 索引
Mysql索引是越多越好嘛? 什么样的字段需要建索引, 什么样的字段不需要 ?
MySQL索引的数量并不是越多越好,过多的索引可能会导致性能下降和存储空间的浪费。
545 0