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


Posted in Javascript onMay 27, 2018

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

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

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

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

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

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

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

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等等,关于拷贝中还有很多问题值得深究,比如正则类型的值如何拷贝,原型上的属性如何拷贝,这些我都会慢慢研究哒!大家也可以思考一下~
最后,欢迎点赞和收藏!!错误之处欢迎指正(`・ω・´)

Javascript 相关文章推荐
FireFox中textNode分片的问题
Apr 10 Javascript
javascript判断两个IP地址是否在同一个网段的实现思路
Dec 13 Javascript
js自动查找select下拉的菜单并选择(示例代码)
Feb 26 Javascript
用json方式实现在 js 中建立一个map
May 02 Javascript
js 通过cookie实现刷新不变化树形菜单
Oct 30 Javascript
轻松学习jQuery插件EasyUI EasyUI实现拖放商品放置购物车
Nov 30 Javascript
JS判断iframe是否加载完成的方法
Aug 03 Javascript
使用JavaScript获取URL中的参数(两种方法)
Nov 16 Javascript
jquery-mobile表单的创建方法详解
Nov 23 Javascript
从源码看angular/material2 中 dialog模块的实现方法
Oct 18 Javascript
Vue父组件向子组件传值以及data和props的区别详解
Mar 02 Javascript
详解Webpack4多页应用打包方案
Jul 16 Javascript
JavaScript面向对象的程序设计(犯迷糊的小羊)
May 27 #Javascript
JS面向对象的程序设计相关知识小结
May 26 #Javascript
JavaScript门道之标准库
May 26 #Javascript
javascript标准库(js的标准内置对象)总结
May 26 #Javascript
简单明了区分escape、encodeURI和encodeURIComponent
May 26 #Javascript
页面点击小红心js实现代码
May 26 #Javascript
js input输入百分号保存数据库失败的解决方法
May 26 #Javascript
You might like
浅析PHP递归函数返回值使用方法
2013/02/18 PHP
JS数学函数Exp使用说明
2012/08/09 Javascript
JS冒泡事件的快速解决方法
2013/12/16 Javascript
JS判断对象是否存在的10种方法总结
2013/12/23 Javascript
js使用ajax读博客rss示例
2014/05/06 Javascript
js实现精美的银灰色竖排折叠菜单
2015/05/16 Javascript
jQuery实现模仿微博下拉滚动条加载数据效果
2015/12/25 Javascript
JavaScript小技巧整理篇(非常全)
2016/01/26 Javascript
JavaScript学习笔记整理_setTimeout的应用
2016/09/19 Javascript
jQuery EasyUI右键菜单实现关闭标签/选项卡
2016/10/10 Javascript
jquery实现(textarea)placeholder自动换行
2016/12/22 Javascript
jQuery基于ajax操作json数据简单示例
2017/01/05 Javascript
Laravel整合Bootstrap 4的完整方案(推荐)
2018/01/25 Javascript
express如何使用session与cookie的方法
2018/01/30 Javascript
JS声明对象时属性名加引号与不加引号的问题及解决方法
2018/02/16 Javascript
JS中promise化微信小程序api
2018/04/12 Javascript
基于mpvue搭建微信小程序项目框架的教程详解
2019/04/10 Javascript
Vue 中文本内容超出规定行数后展开收起的处理的实现方法
2019/04/28 Javascript
微信小程序基于movable-view实现滑动删除效果
2020/01/08 Javascript
react基本安装与测试示例
2020/04/27 Javascript
vue.js 输入框输入值自动过滤特殊字符替换中问标点操作
2020/08/31 Javascript
[40:03]Liquid vs Optic 2018国际邀请赛淘汰赛BO3 第一场 8.21
2018/08/22 DOTA
[40:03]RNG vs VG 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
Python使用reportlab将目录下所有的文本文件打印成pdf的方法
2015/05/20 Python
在Python中过滤Windows文件名中的非法字符方法
2019/06/10 Python
一篇文章搞定Python操作文件与目录
2019/08/13 Python
python 使用递归实现打印一个数字的每一位示例
2020/02/27 Python
英国领先的奢侈品零售商之一:CRUISE
2016/12/02 全球购物
黄色火烈鸟:De Gele Flamingo
2019/03/18 全球购物
马德里运动鞋商店:Nigra Mercato
2020/02/16 全球购物
Hobbs官方网站:英国奢华女性时尚服装
2020/02/22 全球购物
国际经济贸易专业自荐信
2014/06/13 职场文书
滴水洞导游词
2015/02/10 职场文书
亮剑观后感500字
2015/06/05 职场文书
Django 实现jwt认证的示例
2021/04/30 Python
一文搞清楚MySQL count(*)、count(1)、count(col)区别
2022/03/03 MySQL