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 相关文章推荐
js常用函数 不错
Sep 08 Javascript
整理AngularJS框架使用过程当中的一些性能优化要点
Mar 05 Javascript
标准的js无缝滚动效果
Aug 30 Javascript
jQuery实现页面滚动时智能浮动定位
Jan 08 Javascript
基于JavaScript实现窗口拖动效果
Jan 18 Javascript
微信小程序 引入es6 promise
Apr 12 Javascript
angular6.0开发教程之如何安装angular6.0框架
Jun 29 Javascript
layui 监听表格复选框选中值的方法
Aug 15 Javascript
图文讲解vue的v-if使用方法
Feb 11 Javascript
js中数组常用方法总结(推荐)
Apr 09 Javascript
Js代码中的span拼接问题解决
Nov 22 Javascript
js实现自定义滚动条的示例
Oct 27 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的autoLoad自动加载机制
2012/09/27 PHP
Lumen timezone 时区设置方法(慢了8个小时)
2018/01/20 PHP
Yii2框架操作数据库的方法分析【以mysql为例】
2019/05/27 PHP
利用javascript的面向对象的特性实现限制试用期
2011/08/04 Javascript
js图片实时加载提供网页打开速度
2014/09/11 Javascript
详解JavaScript的策略模式编程
2015/06/24 Javascript
关于事件mouseover ,mouseout ,mouseenter,mouseleave的区别
2015/10/12 Javascript
JS声明式函数与赋值式函数实例分析
2016/12/13 Javascript
Vue.js实现一个漂亮、灵活、可复用的提示组件示例
2017/03/17 Javascript
JS中跳出循环的示例代码
2017/09/14 Javascript
JavaScript实现打印星型金字塔功能实例分析
2017/09/27 Javascript
javascript高仿热血传奇游戏实现代码
2018/02/22 Javascript
浅谈Webpack 持久化缓存实践
2018/03/22 Javascript
用vuex写了一个购物车H5页面的示例代码
2018/12/04 Javascript
一次微信小程序内地图的使用实战记录
2019/09/09 Javascript
[01:30]我们共输赢 完美世界城市挑战赛开启全新赛季
2019/04/19 DOTA
Python字符串格式化的方法(两种)
2017/09/19 Python
python实现微信远程控制电脑
2018/02/22 Python
matplotlib.pyplot画图 图片的二进制流的获取方法
2018/05/24 Python
Python 移动光标位置的方法
2019/01/20 Python
python实现对输入的密文加密
2019/03/20 Python
在linux下实现 python 监控usb设备信号
2019/07/03 Python
PyTorch中topk函数的用法详解
2020/01/02 Python
django的模型类管理器——数据库操作的封装详解
2020/04/01 Python
python中sklearn的pipeline模块实例详解
2020/05/21 Python
使用纯 CSS 创作一个脉动 loader效果的源码
2018/09/28 HTML / CSS
Lulu & Georgia官方网站:购买地毯、家具、抱枕、壁纸、床上用品等
2018/03/19 全球购物
火锅店营销方案
2014/02/26 职场文书
光信息科学与技术专业职业生涯规划
2014/03/13 职场文书
大学毕业寄语大全
2014/04/10 职场文书
工作失误检讨书范文
2015/01/26 职场文书
党员“一帮一”活动总结
2015/05/07 职场文书
小学见习报告
2015/06/23 职场文书
会计继续教育培训心得体会
2016/01/19 职场文书
利用python调用摄像头的实例分析
2021/06/07 Python
教你使用RustDesk 搭建一个自己的远程桌面中继服务器
2022/08/14 Servers