手撕深拷贝与浅拷贝

简介: 手撕深拷贝与浅拷贝

什么是浅拷贝?


自己创建一个新对象,这个对象要接收源对象的复制或者引用,如果源对象的属性是基本数据类型,复制的就是基本类型的值给新对象,如果源对象的属性是引用数据类型,则复制的就是内存中的地址,如果一个对象改变了这个内存中的地址,则另一个对象也会改变


如何实现浅拷贝?


方法一: object.assign


assign方法可以用于合并多个对象,也可以用于浅拷贝
Object.assign(参数一,参数二)
let source = {
    a:{ b: 1}
}
let newObj = {}  //自己创建的对象
Object.assign(newObj,source)
console.log(newObj) //{ a: { b: 1 } }
当你修改newObj中的值时,两个对象都会改变
newObj.a.b=2
console.log(newObj)  //{ a: { b: 2 } }
console.log(source)   //{ a: { b: 2 } }


注意:

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝Symbol类型的属性。

Object.assign 循环遍历原对象的属性,通过复制的方式将其赋值给目标对象的相应属性

let obj = { a:{ b:1 }, sym:Symbol(1)}; 
Object.defineProperty(obj, 'innumerable' ,{
    value:'不可枚举属性',
    enumerable:false
});
let newObj = {};
Object.assign(newObj,obj)
obj.a.b = 2;
console.log('obj',obj);
console.log('newObj',newObj);


image.png


方法二:通过扩展运算符


let cloneObj = { ...obj };

如果只是一层数组或是对象,其元素只是简单类型的元素,那么修改第一层的值,另一个对象不会改变,如果是第二层或者更深层,则改变一个值会影响另一个对象

// 对象的拷贝
let obj = {a:1,b:{c:1}}
let newObj = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} 
console.log(newObj); //{a:1,b:{c:1}} a属于第一层数据
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} c是第二层数据
console.log(obj2); //{a:1,b:{c:2}}  


方法三:slice 拷贝数组


slice 方法也比较有局限性,因为它仅仅针对数组类型。slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[ 1, 2, { val: 1000 } ]

从上面这段代码可以看出,利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了。


手工实现浅拷贝


实现思路:

  1. 1.对基础类型做一个最基本的一个拷贝;
  2. 2.对引用类型开辟一个新的存储,并且拷贝一层对象属性。

利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了。


代码如下

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}


深拷贝


什么是深拷贝?


浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。


实现方法


方法一:JSON.stringify

JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。

let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2;
obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}

但是使用 JSON.stringify 实现深拷贝还是有一些地方值得注意

  1. 1.拷贝的对象的值中如果有函数、undefinedsymbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
  2. 2.拷贝 Date 引用类型会变成字符串;
  3. 3.无法拷贝不可枚举的属性;
  4. 4.无法拷贝对象的原型链;
  5. 5.对象中含有 NaNInfinity 以及 -InfinityJSON 序列化的结果会变成 null

针对这些问题,尝试着用下面的这段代码执行一遍,来看看如此复杂的对象,如果用 JSON.stringify 实现深拷贝会出现什么情况。

function Obj() { 
  this.func = function () { alert(1) }; 
  this.obj = {a:1};
  this.arr = [1,2,3];
  this.und = undefined; 
  this.reg = /123/; 
  this.date = new Date(0); 
  this.NaN = NaN;
  this.infinity = Infinity;
  this.sym = Symbol(1);
} 
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{ 
  enumerable:false,
  value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);


image.png


方法二:基础版(手写递归实现)


下面是一个实现 deepClone 函数封装的例子,通过for in遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制

let obj1 = {
  a:{
    b:1
  }
}
function deepClone(obj) { 
  let cloneObj = {}
  for(let key in obj) {                 //遍历
    if(typeof obj[key] ==='object') { 
      cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归
    } else {
      cloneObj[key] = obj[key]  //基本类型的话直接复制值
    }
  }
  return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify 一样,还是有一些问题没有完全解决,例如:

  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 ArrayDateErrorFunction 这样的引用类型并不能正确地拷贝;
  • 对象的属性里面成环,即循环引用没有解决。

这种基础版本的写法也比较简单,可以应对大部分的应用情况。

既然有缺陷,那肯定就有更好的方案


方法三:改进版(改进后递归实现)


针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。

  1. 1.针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用Reflect.ownKeys 方法;
  2. 2.当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
  3. 3.利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
  4. 4.利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏,作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
  return new Date(obj)       // 日期对象直接返回一个新的日期对象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     //正则对象直接返回一个新的正则对象
  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}

下面开始验证验证代码

let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)


目录
相关文章
|
12天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1262 5
|
1天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
11天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1280 87
|
12天前
|
云栖大会
阿里云云栖大会2025年9月24日开启,免费申请大会门票,速度领取~
2025云栖大会将于9月24-26日举行,官网免费预约畅享票,审核后短信通知,持证件入场
1823 13