其他分享
首页 > 其他分享> > js中的浅拷贝与深拷贝

js中的浅拷贝与深拷贝

作者:互联网

一、引言

  深拷贝浅拷贝是一个老生常谈的问题了,那么,到底什么是深拷贝,什么是浅拷贝,又如何实现一个深拷贝。今天,我们就来谈一谈这些问题。首先,我推荐你阅读我之前写过的一篇文章js的栈内存和堆内存,其实,它就是深浅拷贝的底层原理,话不多说,我们现在开始。

二、深拷贝和浅拷贝

  如果你仔细阅读了我之前的那篇文章,相信你已经很了解js中的两种内存了,而深浅拷贝正是与他们的存储方式有关。
  对于基本类型来说,他们存储在栈内存中。复制基本类型就是将其值复制一份再存储到栈内存中,所以对于基本类型来说,不存在深拷贝或者浅拷贝。
  而对于引用类型来说,如果你简单的复制一份,实际上是复制的引用类型在堆内存中存储的地址。所以,如果你修改某个属性,实际上是通过存储在栈内存中的地址找到堆内存中的对象,进而修对象中的某个属性。而实际上复制之后的和之前的指向了同一个对象,也就意味着不管哪个修改了某个属性都会同时体现在两个变量上,这就是浅拷贝。那么什么是深拷贝呢?当你复制一个引用类型时,不是简单的复制它的存储地址,而是创造一个新的对象,将老对象的属性都复制一份。这样,你修改两个变量中的某一个就不会对另一个产生影响,这就是深拷贝。
  因为之前的那篇文章对于这块讲的已经比较清楚了。只是没有引出深拷贝和浅拷贝的概念而已,所以在这里我们只是用文字去描述了深浅拷贝,相信真正理解了栈内存和堆内存的同学应该很快就能接受这两个概念。好了,那么既然我们知道了什么是深拷贝什么是浅拷贝,我们应该如何去实现一个深拷贝呢?

三、实现一个深拷贝

  1. 最简单的办法:
function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

  只需要短短的一行代码,我们就能实现一个深拷贝。我们来看看效果:

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

let obj = {1:1, 2:2, 3:{4:4}}
let obj1 = obj;
let obj2 = deepClone(obj);
obj1[3][4] = "obj1";
obj2[3][4] = "obj2"
console.log(obj[3][4]);
//obj1

  可以看到,我们修改obj2的属性值之后,并没有影响到原来的obj,所以这的确是一个可用的深拷贝。那么,他有什么问题?注意,我们这种方法是将其变成一个JSON对象,然后再转变回js对象实现的深拷贝,很显然,问题就出在js对象变成JSON对象的过程中。我们知道,JSON对象的值必须是以下几种之一: 数字、字符串、逻辑值、数组、对象、null。而在js中,数据类型显然不止这几种,比如说:undefined, function。如果我们的对象存在这两中数据类型会发生什么呢?我们来试试:

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

let obj = {1:undefined, 2:function(){}, 3:{4:4}}
let obj2 = deepClone(obj);
console.log(obj2);
//{ '3': { '4': 4 } }

  可以看到, 拷贝后1、2的值直接不见了,可见我们无法对JSON值之外的数据类型进行拷贝,如果我们想拷贝JSON值之外的数据类型,那么就需要我们自己手写一个深拷贝了。
2. 递归实现深拷贝

function deepClone(target) {
  if(typeof target === 'object' && target !== null) {
    let clone = Array.isArray(target) ? [] : {};
    for(let key in target) {
      clone[key] = deepClone(target[key]);
    }
    if(target === null) console.log(clone);
    return clone;
  }else {
    return target;
  }
}
let obj = {1:null, 2:function(){console.log("hello");}, 3:{4:[1, 2, 3, 4]}}
let obj2 = deepClone(obj);
console.log(obj2);
//{ '1': null, '2': [Function: 2], '3': { '4': [ 1, 2, 3, 4 ] } }

  我们判断传入的参数是否是对象,如果是对象,则再次进行拷贝,否则,直接返回。同时,这里有两点需要特殊处理。一是null也会被typeof判断成object,但是他不需要再次拷贝,所以我们可以直接返回。而是我们不能确定传入的参数是对象还是数组,所以我们加一个判断条件,根据不同的情况给clone赋不同的值。可以看到,现在我们的深拷贝已经能够正常拷贝null和funtion了,那么,他真的一点问题都没有了吗?我们看下面这种情况:

function deepClone(target) {
  if(typeof target === 'object' && target !== null) {
    let clone = Array.isArray(target) ? [] : {};
    for(let key in target) {
      clone[key] = deepClone(target[key]);
    }
    if(target === null) console.log(clone);
    return clone;
  }else {
    return target;
  }
}
let obj = {1:null, 2:function(){console.log("hello");}, 3:{4:[1, 2, 3, 4]}}
obj[4] = obj;
let obj2 = deepClone(obj);
console.log(obj2);

在这里插入图片描述  这里我只是稍微改了一个地方,即obj[4] = obj。而正是因为这个小改动造成了循环引用的问题,也就导致了栈溢出,那么,我们如何解决这个问题呢?很简单,我们把已经拷贝过的对象存起来,key为原对象,value为拷贝后的对象。如果再次拷贝到了这个对象,我们就不用再次拷贝了,直接赋值就可以了。而普通的对象key值只能是字符串,所以我们用Map对象来帮我们实现这个需求。

function deepClone(target, map) {
  if(typeof target === 'object' && target !== null) {
    let clone = Array.isArray(target) ? [] : {};
    if(map.get(target)) {
      return map.get(target);
    }
    map.set(target, clone);
    for(let key in target) {
      clone[key] = deepClone(target[key], map);
    }
    return clone;
  }else {
    return target;
  }
}
let obj = {1:null, 2:function(){console.log("hello");}, 3:{4:[1, 2, 3, 4]}}
obj[4] = obj;
let obj2 = deepClone(obj, new Map());
console.log(obj2);
//{ '1': null, '2': [Function: 2], '3': { '4': [ 1, 2, 3, 4 ] }, '4': [Circular] }
obj2[4][2]()
//hello

  好了,可以看到我们已经正常的将其进行了拷贝而没有报错,至于4中[Circular],应该是循环引用的对象都会这样显示,我们可以调用一下funtion看是否能正常输出,我们可以看到是可以输出hello的,所以证明这是已经拷贝成功了。

四、结语

  其实,这个深拷贝还是有问题,我们只是判断了null和funtion两种特殊类型,其实还有其他的类型,比如Date对象,正则对象,Error对象等,有兴趣的同学可以自己继续完善。实际上,要实现一个非常完美的深拷贝是很困难的,需要考虑许多东西。同时,如果你真的有这种需求,不妨想想为什么会有这种需求,有没有其他的方法可以规避掉,这才是应对深拷贝的正确方式。

标签:obj,target,js,deepClone,let,拷贝,null
来源: https://blog.csdn.net/qq_38164763/article/details/100876148