浅拷贝与深拷贝

浅拷贝的原理和实现

对于浅拷贝的定义我们可以初步理解为:

自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象。

实现浅拷贝的方法

object.assign

object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
语法:object.assign 的语法为:Object.assign(target, …sources)

var obj = { a: 1 };
var obj1 = { b: { c: 1 } };
var obj2 = { d: 5 };
console.log(Object.assign(obj, obj1)); //{a:1,b:{c:1}}
console.log(Object.assign(obj, obj1, obj2)); // {a:1,b:{c:1},d:5}
var obj = {};
var obj1 = { a: 1, b: { c: 1 } };
Object.assign(obj, obj1);
console.log(obj); //{b:{c:10}}
obj1.a = 2;
console.log(obj); //{ a: 1, b: { c: 1 } }
console.log(obj1); //{ a: 2, b: { c: 1 } }
var obj = {};
var obj1 = { a: 1, b: { c: 1 } };
Object.assign(obj, obj1);
console.log(obj); //{b:{c:10}}
obj1.b.c = 10;
console.log(obj); //{a:1,b:{c:10}}
console.log(obj1); //{a:1,b:{c:10}}

使用Object.assign的注意点

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝 Symbol 类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)}; 
// 添加一个不可枚举属性
Object.defineProperty(obj1, 'innumerable' ,{
    value:'不可枚举属性',
    enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1) 
obj1.a.b = 2;
console.log('obj1',obj1);//{ a:{ b:2 }, sym:Symbol(1),innumerable:'不可枚举属性'}
console.log('obj2',obj2);//{ a:{ b:2 }, sym:Symbol(1)}
扩展运算符方式
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) // {a:1,b:{c:1}}
console.log(obj2) // {a:2,b:{c:1}}
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.b.c = 3
console.log(obj) // {a:1,b:{c:3}}
console.log(obj2) // {a:1,b:{c:3}}
concat拷贝数组
let arr = [1,2,3,[4]]
let newArr = arr.concat()
newArr[0] = 100
console.log(arr) // [1,2,3,[4]]
console.log(newArr) // [100,2,3,[4]]
let arr = [1,2,3,[4]]
let newArr = arr.concat()
newArr[3][0] = 50
console.log(arr) // [1,2,3,[50]]
console.log(newArr) // [1,2,3,[50]]
slice拷贝数组
let arr = [1,2,{val:4}]
let newArr = arr.slice()
arr[0] = 50
console.log(arr) // [50,2,{val:4}]
console.log(newArr) //[1,2,{val:4}]
let arr = [1,2,{val:4}]
let newArr = arr.slice()
arr[2].val = 100
console.log(arr) // [1,2,{val:100}]
console.log(newArr) // [1,2,{val:100}]

总结:从上面的代码中可以看出,这就是浅拷贝的限制所在了——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。

深拷贝的原理和实现

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

这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下:

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

深拷贝的方法

乞丐版(JSON.stringify)

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

let arr = [1, 2, [3, 4, 5]];
let arr1 = JSON.parse(JSON.stringify(arr));
arr[2].push(6);
console.log(arr); // [1, 2, [3, 4, 5, 6]]
console.log(arr1); // [1, 2, [3, 4, 5]]

从上面的代码可以看到,通过 JSON.stringify 可以初步实现一个数组的深拷贝,通过改变 arr 的 arr[2],其实可以看出 arr1 这个数组也不受影响。

使用JSON.stringify值得注意的点

  1. 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
  2. 拷贝 Date 引用类型会变成字符串;
  3. 无法拷贝不可枚举的属性;
  4. 无法拷贝对象的原型链;
  5. 拷贝 RegExp 引用类型会变成空对象;
  6. 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  7. 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
let obj = {
    a: undefined,
    b: function () {},
    c: Symbol(1),
    d: new Date(),
    e: /runoob/i,
    f: NaN,
    g: Infinity,
    h: {
        i: "嘿嘿嘿",
    },
};
Object.defineProperty(obj, "innumerable", {
    value: "不可枚举属性",
    enumerable: false,
});
console.log()
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj1);

在这里插入图片描述
在这里插入图片描述
使用 JSON.stringify 方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify 暂时还是无法满足的.

基础版(手写递归)
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}}
console.log(obj1);   //{a:{b:2}}

缺点

  1. 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
  2. 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
  3. 对象的属性里面成环,即循环引用没有解决。
改进版(改进后递归实现)
  1. 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
  2. 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
  3. 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;
  4. 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 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)

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兢兢业业的码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值