js深浅拷贝-深拷贝,浅拷贝

浅拷贝的原理和实现

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

浅拷贝只能拷贝一层

浅拷贝,对象拷贝的时候,基本数据类型不会随之变化,拷贝对象中引用数据类型会出现拷贝后随之改变

方法一:object.assign

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

object.assign 的语法为:Object.assign(target, ...sources)


let target = {};

let source = { a: { b: 1 } };

Object.assign(target, source);

console.log(target); // { a: { b: 1 } };

下面的例子中有点不同:


let target = {};

let source = { a: { b: 2 } };

Object.assign(target, source);

console.log(target); // { a: { b: 10 } }; 

source.a.b = 10; 

console.log(source); // { a: { b: 10 } }; 

console.log(target); // { a: { b: 10 } };

但是使用 object.assign 方法有几点需要注意:

它不会拷贝对象的继承属性;

它不会拷贝对象的不可枚举的属性;

可以拷贝 Symbol 类型的属性。
可以简单理解为: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);

console.log('obj2',obj2);

VM1652:17 obj1 {a: {…}, sym: Symbol(1), innumerable: "不可枚举属性"}a: {b: 2}sym: Symbol(1)innumerable: "不可枚举属性"__proto__: Object
VM1652:19 obj2 

方法二:扩展运算符方式

我们也可以利用 JS 的扩展运算符,在构造对象的同时完成浅拷贝的功能。

扩展运算符的语法为:let cloneObj = { ...obj };
/* 对象的拷贝 */

let obj = {a:1,b:{c:1}}

let obj2 = {...obj}

obj.a = 2

console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}

obj.b.c = 2

console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}

/* 数组的拷贝 */

let arr = [1, 2, 3];

let newArr = [...arr]; //跟arr.slice()是一样的效果

方法三:concat 拷贝数组

数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。

let arr = [1, 2, 3];

let newArr = arr.concat();

newArr[1] = 100;

console.log(arr);  // [ 1, 2, 3 ]

console.log(newArr); // [ 1, 100, 3 ]

方法四:slice 拷贝数组

手工实现一个浅拷贝:

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.stringfy)

JSON.stringfy() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 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]}

1、拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;

2、拷贝 Date 引用类型会变成字符串;

3、无法拷贝不可枚举的属性;

4、无法拷贝对象的原型链;

5、拷贝 RegExp 引用类型会变成空对象;

6、对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

7、无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

方法二:基础版

下面是一个实现 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}}

缺点:
这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;

这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;

对象的属性里面成环,即循环引用没有解决。

方法三:改进版

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

针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;

当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;

利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链;

利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。

关于第 4 点的 WeakMap,这里我不进行过多的科普讲解了,你如果不清楚可以自己再通过相关资料了解一下。我也经常在给人面试中看到有人使用 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)

map 与weakmap

我们常用的对象Object,是由key:value集合组成的,但key只能是字符串,有很大的使用限制。当我们需要其他类型的数据做key值时,就需要用到数据结构Map,它支持把各种类型的值,当做键。

var map = new Map();
var nie = {name:'Annie'};
var kth = {name:'Keith'};
map.set(nie,kth);
map.set(kth,nie);
console.log(map.get(nie));    //{name:'Keith'}
console.log(map.get(kth));    //{name:'Annie'}

map.size    //2,表示map中的成员数量
map.has(nie)    //true,布尔值,判断成员是否存在
map.delete(nie)    //true,布尔值,删除成员,删除成功返回true,失败返回false
map.clear()    //清除所有成员,没有返回值

WeakMap与Map类似,但有几点区别:

1、WeakMap只接受对象作为key,如果设置其他类型的数据作为key,会报错。

2、WeakMap的key所引用的对象都是弱引用,只要对象的其他引用被删除,垃圾回收机制就会释放该对象占用的内存,从而避免内存泄漏。

3、由于WeakMap的成员随时可能被垃圾回收机制回收,成员的数量不稳定,所以没有size属性。

4、没有clear()方法

5、不能遍历

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值