javascript深拷贝、浅拷贝和循环引用深入理解

一、为什么有深拷贝和浅拷贝?

这个要从js中的数据类型说起,js中数据类型分为基本数据类型和引用数据类型。

基本类型值指的是那些保存在栈内存中的简单数据段,即这种值是完全保存在内存中的一个位置。包含Number,String,Boolean,Null,Undefined ,Symbol。

引用类型值指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。除了上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有:Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型 等。

正因为引用类型的这种机制, 当我们从一个变量向另一个变量复制引用类型的值时,实际上是将这个引用类型在栈内存中的引用地址复制了一份给新的变量,其实就是一个指针。因此当操作结束后,这两个变量实际上指向的是同一个在堆内存中的对象,改变其中任意一个对象,另一个对象也会跟着改变。

因此深拷贝和浅拷贝只发生在引用类型中。简单来说他们的区别在于:

1. 层次

  • 浅拷贝 只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会赋值目标对象的第一层属性。
  • 深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。

2. 是否开辟新的栈

  • 浅拷贝 对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」,并没有开辟新的栈,也就是复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变,
  • 深拷贝 而深复制则是开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

二、浅拷贝

以下是实现浅拷贝的几种实现方式:

1.Array.concat()

  const arr = [1,2,3,4,[5,6]];
  const copy = arr.concat(); \\ 利用concat()创建arr的副本

  \\改变基本类型值,不会改变原数组
  copy[0] = 2;
  arr; //[1,2,3,4,[5,6]];

  \\改变数组中的引用类型值,原数组也会跟着改变
  copy[4][1] = 7;
  arr; //[1,2,3,4,[5,7]];
  

能实现类似效果的还有slice()和Array.from()等,大家可以自己尝试一下~

2.Object.assign()

const obj1 = {x: 1, y: 2};
const obj2 = Object.assign({}, obj1);

obj2.x = 2; \\修改obj2.x,改变对象中的基本类型值
console.log(obj1) //{x: 1, y: 2} //原对象未改变
console.log(obj2) //{x: 2, y: 2}
const obj1 = {
  x: 1,
  y: {
    m: 1
  }
};
const obj2 = Object.assign({}, obj1);

obj2.y.m = 2; \\修改obj2.y.m,改变对象中的引用类型值
console.log(obj1) //{x: 1, y: {m: 2}} 原对象也被改变
console.log(obj2) //{x: 2, y: {m: 2}}

三、深拷贝

1.JSON.parse()和JSON.stringify()

const obj1 = {
  x: 1,
  y: {
    m: 1
  }
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}

obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}} 原对象未改变
console.log(obj2) //{x: 2, y: {m: 2}}

这种方法使用较为简单,可以满足基本日常的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是有以下几个缺点:

  • undefined、任意的函数、正则表达式类型以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时);
  • 它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;
  • 如果对象中存在循环引用的情况无法正确处理。

2.递归

function deepCopy1(obj) {
  // 创建一个新对象
  let result = {}
  let keys = Object.keys(obj),
    key = null,
    temp = null;

  for (let i = 0; i < keys.length; i++) {
    key = keys[i];
    temp = obj[key];
    // 如果字段的值也是一个对象则递归操作
    if (temp && typeof temp === 'object') {
      result[key] = deepCopy(temp);
    } else {
    // 否则直接赋值给新对象
      result[key] = temp;
    }
  }
  return result;
}

const obj1 = {
  x: {
    m: 1
  },
  y: undefined,
  z: function add(z1, z2) {
    return z1 + z2
  },
  a: Symbol("foo")
};

const obj2 = deepCopy1(obj1);
obj2.x.m = 2;

console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}

四、循环引用

看似递归已经完全解决我们的问题了,然而还有一种情况我们没考虑到,那就是循环引用

1.父级引用

这里的父级引用指的是,当对象的某个属性,正是这个对象本身,此时我们如果进行深拷贝,可能会在子元素->父对象->子元素...这个循环中一直进行,导致栈溢出。比如下面这个例子:

 const obj1 = {
  x: 1,
  y: 2
};
obj1.z = obj1;

const obj2 = deepCopy1(obj1); \\栈溢出

解决办法是:只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,可以修改上面的deepCopy函数:

function deepCopy2(obj, parent=null) {
  //创建一个新对象
  let result = {};
  let keys = Object.keys(obj),
     key = null,
     temp = null,
     _parent = parent;
  //该字段有父级则需要追溯该字段的父级
  while(_parent) {
    //如果该字段引用了它的父级,则为循环引用
    if(_parent.originParent === obj) {
      //循环引用返回同级的新对象
      return _parent.currentParent;
    }
    _parent = _parent.parent
  }
  for(let i=0,len=keys.length;i<len;i++) {
    key = keys[i]
    temp = obj[key]
    // 如果字段的值也是一个新对象
    if(temp && typeof temp === 'object') {
      result[key] = deepCopy(temp, {
        //递归执行深拷贝,将同级的待拷贝对象与新对象传递给parent,方便追溯循环引用
        originParent: obj,
        currentParent: result,
        parent: parent
      });
    } else {
      result[key] = temp;
    }
  }
  return result;
}

const obj1 = {
  x:1
}
obj1.z = obj1;

const obj2 = deepCopy2(obj1);

2. 同级引用

假设对象obj有a,b,c三个子对象,其中子对象c中有个属性d引用了对象obj下面的子对象a。

const obj= {
  a: {
    name: 'a'
  },
  b: {
    name: 'b'
  },
  c: {

  }
};
c.d.e = obj.a;

此时c.d.e和obj.a 是相等的,因为它们引用的是同一个对象

console.log(c.d.e === obj.a); //true

如果我们调用上面的deepCopy2函数

const copy = deepCopy2(obj);
console.log(copy.a); // 输出: {name: "a"}
console.log(copy.d.e);// 输出: {name: "a"}
console.log(copy.a === copy.d.e); // 输出: false

以上表现我们就可以看出,虽然opy.a 和copy.d.e在字面意义上是相等的,但二者并不是引用的同一个对象,这点上来看对象copy和原对象obj还是有差异的。

这种情况是因为obj.a并不在obj.d.e的父级对象链上,所以deepCopy2函数就无法检测到obj.d.e对obj.a也是一种引用关系,所以deepCopy2函数就将obj.a深拷贝的结果赋值给了copy.d.e。

解决方案:父级的引用是一种引用,非父级的引用也是一种引用,那么只要记录下对象A中的所有对象,并与新创建的对象一一对应即可。

function deepCopy3(obj) {
  // hash表,记录所有的对象的引用关系
  let map = new WeakMap();
  function dp(obj) {
    let result = null;
    let keys = Object.keys(obj);
    let key = null,
      temp = null,
      existobj = null;

    existobj = map.get(obj);
    //如果这个对象已经被记录则直接返回
    if(existobj) {
      return existobj;
    }

    result = {}
    map.set(obj, result);

    for(let i =0,len=keys.length;i<len;i++) {
      key = keys[i];
      temp = obj[key];
      if(temp && typeof temp === 'object') {
        result[key] = dp(temp);
      }else {
        result[key] = temp;
      }
    }
    return result;
  }
  return dp(obj);
}

const obj= {
  a: {
    name: 'a'
  },
  b: {
    name: 'b'
  },
  c: {

  }
};
c.d.e = obj.a;

const copy = deepCopy3(obj);

五、总结

其实拷贝的方式还有很多种,比如jquery中的$.extend,lodash的_.cloneDeep等等,关于拷贝中还有很多问题值得深究,比如正则类型的值如何拷贝,原型上的属性如何拷贝,这些我都会慢慢研究哒!大家也可以思考一下~
最后,欢迎点赞和收藏!!错误之处欢迎指正(`・ω・´)

(0)

相关推荐

  • Javascript 浅拷贝、深拷贝的实现代码

    什么是"clone"? 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的.在Java/javasript语言中,用简单的赋值语句是不能满足这种需求的.要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段,当然了 javascript语言中并没有此方法. 所以我特意写了两

  • javascript深拷贝和浅拷贝详解

    一.数组的深浅拷贝 在使用JavaScript对数组进行操作的时候,我们经常需要将数组进行备份,事实证明如果只是简单的将它赋予其他变量,那么我们只要更改其中的任何一个,然后其他的也会跟着改变,这就导致了问题的发生. 这是为什么呢? 因为如果只是简单的赋值,它只是进行了地址的引用,所以改变一个另一个也会跟着变. var arr = ["One","Two","Three"]; var arrto = arr; arrto[1] = "te

  • JavaScript实现浅拷贝与深拷贝的方法分析

    本文实例讲述了JavaScript实现浅拷贝与深拷贝的方法.分享给大家供大家参考,具体如下: 平时使用数组复制时,我们大多数会使用'=',这只是浅拷贝,存在很多问题.比如 let arr = [1,2,3,4,5]; let arr2 = arr; console.log(arr) //[1, 2, 3, 4, 5] console.log(arr2) //[1, 2, 3, 4, 5] arr[0] = 6; console.log(arr) //[6, 2, 3, 4, 5] console

  • javascript二维数组和对象的深拷贝与浅拷贝实例分析

    本文实例讲述了javascript二维数组和对象的深拷贝与浅拷贝.分享给大家供大家参考,具体如下: 这篇文章主要为大家详细介绍了js实现数组和对象的深浅拷贝, 1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用 2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的"值"(数组的所有元素)拷贝过来,是"值"而不是"引用" JavaScript的数据类型分为原始数据类型和对象类型.二者在内存中存放的方式

  • JS浅拷贝和深拷贝原理与实现方法分析

    本文实例讲述了JS浅拷贝和深拷贝原理与实现方法.分享给大家供大家参考,具体如下: 浅拷贝只会拷贝一层,深层的引用类型改变还是会受到影响. 深拷贝是所有内部的属性还有值都被拷贝了一份,不管深层的引用类型怎么改都不会受到影响. 浅拷贝的实现方式 1.自定义函数 function shallowClone (initalObj) { var obj = {}; for ( var i in initalObj) { obj[i] = initalObj[i]; } return obj; } 2.ES

  • 浅析javaScript中的浅拷贝和深拷贝

    1.javaScript的变量类型 (1)基本类型: 5种基本数据类型Undefined.Null.Boolean.Number 和 String,变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问. (2)引用类型: 存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置.当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据. JavaScript存储对象都是存地址的,所以浅拷贝会导致 obj1 和obj2 指向同一

  • JS中实现浅拷贝和深拷贝的代码详解

    (一)JS中基本类型和引用类型 JavaScript的变量中包含两种类型的值:基本类型值 和 引用类型值,在内存中的表现形式在于:前者是存储在栈中的一些简单的数据段,后者则是保存在堆内存中的一个对象. 基本类型值 在JavaScript中基本数据类型有 String , Number , Undefined , Null , Boolean ,在ES6中,又定义了一种新的基本数据类型 Symbol ,所以一共有6种. 基本类型是按值访问的,从一个变量复制基本类型的值到另一个变量后,这两个变量的值

  • JavaScript深拷贝和浅拷贝概念与用法实例分析

    本文实例讲述了JavaScript深拷贝和浅拷贝概念与用法.分享给大家供大家参考,具体如下: js中的浅拷贝和深拷贝,只是针对复杂数据类型(Objcet,Array)的复制问题.简单来讲浅拷贝和深拷贝都可以实现在原有对象的基础上再生成一份的作用.但是根据新生成的对象能否影响到原对象可以分为浅拷贝和深拷贝. 概念1:浅拷贝 浅拷贝就是指拷贝引用,新生成的引用和原来的引用都是指向同一个对象的实例,彼此之间的操作会相互影响. 概念2:深拷贝 在堆中重新开辟内存,把原引用对应的对象实例中所有的内容进行拷

  • JavaScript对象的浅拷贝与深拷贝实例分析

    本文实例讲述了JavaScript对象的浅拷贝和深拷贝.分享给大家供大家参考,具体如下: 1.浅拷贝 仅仅复制对象的引用,而不是对象本身. var person = { name: 'Alice', friends: ['Bruce', 'Cindy'] } var student = { id: 30 } student = simpleClone(person, student); student.friends.push('David'); alert(person.friends); f

  • JavaScript基础心法 深浅拷贝(浅拷贝和深拷贝)

    前言 说到深浅拷贝,必须先提到的是JavaScript的数据类型,之前的一篇文章JavaScript基础心法--数据类型说的很清楚了,这里就不多说了. 需要知道的就是一点:JavaScript的数据类型分为基本数据类型和引用数据类型. 对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的. 浅拷贝 浅拷贝的意思就是只复制引用,而未复制真正的值. const originArray = [1,2,3,4,5]; const originObj = {a:'a'

  • JavaScript数组深拷贝和浅拷贝的两种方法

    例如这个例子: 复制代码 代码如下: var arr = ["One","Two","Three"]; var arrto = arr;arrto[1] = "test";document.writeln("数组的原始值:" + arr + "<br />");//Export:数组的原始值:One,test,Threedocument.writeln("数组的新值

  • javascript对浅拷贝和深拷贝的详解

    下面小编就为大家带来一篇浅谈JavaScript中面向对象的的深拷贝和浅拷贝.小编觉得挺不错的,现在就分享给大家,也给大家做个参考. 1.浅拷贝:复制一份引用,所有引用对象都指向一份数据,并且都可以修改这份数据. 2.深拷贝(复杂):复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制. 这里画一个简单的图来加深理解: 一.数组的深浅拷贝 在使用JavaScript对数组进行操作的时候,我们经常需要将数组进行备份,事实证明如果只是简单的将它赋予其他变量,那么我们只要更改其中的任何一个

  • js对象浅拷贝和深拷贝详解

    本文为大家分享了JavaScript对象的浅拷贝和深拷贝代码,供大家参考,具体内容如下 1.浅拷贝 拷贝就是把父对像的属性,全部拷贝给子对象. 下面这个函数,就是在做拷贝: var Chinese = { nation:'中国' } var Doctor = { career:'医生' } function extendCopy(p) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; } 使用的时候,这样写

  • javascript 关于赋值、浅拷贝、深拷贝的个人理解

    一.栈.堆.指针地址 栈内存:个人理解是,基本数据类型和引用数据类型都会用到的一个空间,这个空间以key-value形式存在,value本身不可修改,只能赋值替换: 堆内存:堆,就是堆积,每一个被开辟的空间可以想象成一个空纸盒子,纸盒子所在的纸盒子堆就是 "堆" .基本数据类型没有堆的概念.堆,只针对引用数据类型.存储方式应该是以对象(object)形式保存,对象内容包含key-value形式数据,value本身同样不可修改,只能赋值替换: 指针地址:针对引用数据类型在栈保存的值就是指

  • 浅谈JavaScript中面向对象的的深拷贝和浅拷贝

    理解深拷贝和浅拷贝之前需要弄懂一些基础概念,内存中存储的变量类型分为值类型和引用类型. 1.值类型赋值的存储特点, 将变量内的数据全部拷贝一份, 存储给新的变量. 例如:var num = 123 :var num1=num; 表示变量中存储的数字是 123.然后将数据拷贝一份,就是将 123 拷贝一份. 那么内存中有 2 个 数组;将拷贝数据赋值给 num2,其特点是在内存中有两个数据副本.这可以理解为浅拷贝. 2.引用类型的赋值. var o={name:'张三'}: var obj=o;

随机推荐