详解JS深拷贝与浅拷贝


Posted in Javascript onAugust 04, 2020

一、预备知识

1.1、JS数据类型

基本数据类型:Boolean、String、Number、null、undefined
引用数据类型:Object、Array、Function、RegExp、Date等

1.2、数据类型的复制

基本数据类型的复制,是按值传递的

var a = 1;
var b = a;
b = 2;
console.log(a); // 1
console.lob(b); // 2

引用数据类型的复制,是按引用传值

var obj1 = {
 a: 1;
 b: 2;
};
var obj2 = obj1;
obj2.a=3;
console.log(obj1.a); //3
console.log(obj2.a); // 3

1.3、深拷贝与浅拷贝

深拷贝和浅拷贝都只针对引用数据类型,浅拷贝会对对象逐个成员依次拷贝,但只复制内存地址,而不复制对象本身,新旧对象成员还是共享同一内存;深拷贝会另外创建一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

区别:浅拷贝只复制对象的第一层属性,而深拷贝会对对象的属性进行递归复制。

二、JS浅拷贝

2.1、赋值与浅拷贝

当把一个对象赋值给一个新的变量时,赋的对象是该对象在栈中的地址,而不是堆中的数据。也就是新旧两个对象指的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,两个对象联动的会一起改变。

var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

详解JS深拷贝与浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,对原有对象的成员进行依次拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。因此如果新对象中的某个对象成员改变了地址,就会影响到原有的对象。

//手写浅拷贝
function shallowCopy(obj1) {
 let obj2 = Array.isArray(obj1) ? [] : {}
 for (let i in obj1) {
  obj2[i] = obj1[i]
 }
 return obj2
}
var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = shallowCopy(obj1);
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

详解JS深拷贝与浅拷贝

2.2、浅拷贝的实现

(1)Object.assign()

Object.assign()方法可以把源对象自身的任意多个的可枚举属性拷贝给目标对象,然后返回目标对象,但是Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。此方法对于Array和Object均可适用。

var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = Object.assign({}, obj1);
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

详解JS深拷贝与浅拷贝

(2)Array.prototype.concat()和Array.prototype.slice()

Array.prototype.concat()和Array.prototype.slice()均为Array原型上的方法,只适用于Array。

var arr1 = [1,3,{
 user: 'aaa'
}]
var arr2 = arr1.concat();
arr2[0] = '一';
arr2[2].user = 'AAA';
console.log('arr1',arr1)
console.log('arr2',arr2)


var arr1 = [1,3,{
 user: 'aaa'
}]
var arr2 = arr1.slice();
arr2[0] = '一';
arr2[2].user = 'AAA';
console.log('arr1',arr1)
console.log('arr2',arr2)

详解JS深拷贝与浅拷贝

补充说明:Array的slice和contact方法都不会修改原数组,而是会返回一个对原数组进行浅拷贝的新数组。这两种方法同Object.assign()一样,都是对第一层属性依次拷贝,如果第一层的属性是基本数据类型,就拷贝值;如果是引用数据类型,就拷贝内存地址。

三、JS深拷贝

对对象的属性中所有引用类型的值,遍历到是基本类型的值为止。

3.1、深拷贝实现方式

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

原理:用JSON.stringify()将对象转成字符串,再用JSON.parse()把字符串解析成对象。

var obj1 = {
  'name' : 'zhangsan',
  'language' : [1,[2,3],[4,5]],
};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

详解JS深拷贝与浅拷贝

缺点:这种方法可以实现数组和对象和基本数据类型的深拷贝,但不能处理函数。因为JSON.stringify()方法是将一个javascript值转换我一个JSON字符串,不能接受函数。其他影响如下:

  • 如果对象中有时间对象,那么用该方法拷贝之后的对象中,时间是字符串形式而不是时间对象
  • 如果对象中有RegExp、Error对象,那么序列化的结果是空
  • 如果对象中有函数或者undefined,那么序列化的结果会把函数或undefined丢失
  • 如果对象中有NAN、infinity、-infinity,那么序列化的结果会变成null
  • JSON.stringfy()只能序列化对象的可枚举自有属性,如果对象中有是构造函数生成的,那么拷贝后会丢弃对象的constructor
  • 如果对象中存在循环引用也无法正确实现深拷贝

(2)手写深拷贝函数

通过递归实现深拷贝

function deepCopy(obj){
 var result= Array.isArray(obj) ? [] : {}
 if (obj && typeof(obj) === 'object') {
  for (let i in obj) {
   if (obj.hasOwnProperty(i)){ // 思考:这句是否有必要?
    if (obj[i] && typeof(obj[i]) === 'object') {
     result[i] = deepCopy(obj[i])
    } else {
     result[i] = obj[i]
    }
   }
  }
 }
 return result
}
var obj1 = {
 a: 1,
 b: {
  c: 2
 }
};
var obj2 = deepCopy(obj1);
obj2.a = '一';
obj2.b.c = '二'
console.log('obj1', obj1)
console.log('obj2', obj2)

obj.hasOwnProperty(prop)用来判断obj这个对象中是否含有prop这个属性,返回布尔值,有则true,没有则false

以上有个缺陷:当遇到两个互相引用的对象时,会出现死循环的情况,从而导致爆栈。为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否互相引用。

深拷贝函数改进(防止循环递归爆栈)

function deepCopy(obj, parent = null) {
 let result = Array.isArray(obj) ? [] : {}
 let _parent = parent
 // 该字段有父级则需要追溯该字段的父级
 while(_parent) {
  // 如果该字段引用了它的父级,则为循环引用
  if (_parent.originalParent === obj) {
   // 循环引用返回同级的新对象
   return _parent.currentParent 
  }
  _parent = _parent.parent
 }
 if (obj && typeof(obj) === 'object') {
  for (let i in obj) {
   // 如果字段的值也是一个对象
   if (obj[i] && typeof(obj[i]) === 'object') {
    // 递归执行深拷,将同级的待拷贝对象传递给parent,方便追溯循环引用
    result[i] = deepCopy(obj[i], {
     originalParent: obj,
     currentParent: result,
     parent: parent
    })
   } else {
    result[i] = obj[i]
   }
  }
 }
 return result
}
var obj1 = {
 x: 1,
 y: 2
};
obj1.z = obj1
var obj2 = deepCopy(obj1)
console.log('obj1', obj1)
console.log('obj2', obj2)

以上代码可以复制到浏览器去试试吧

深拷贝函数最终版(支持基本数据类型、Array、Object、原型链、RegExp、Date类型)

function deepCopy(obj, parent = null) {
 let result
 let _parent = parent
 while(_parent) {
  if (_parent.originalParent === obj) {
   return _parent.currentParent
  }
  _parent = _parent.parent
 }
 if (obj && typeof(obj) === 'object') {
  if (obj instanceof RegExp) {
   result = new RegExp(obj.source, obj.flags)
  } else if (obj instanceof Date) {
   result = new Date(obj.getTime())
  } else {
   if (obj instanceof Array) {
    result = []
   } else {
    let proto = Object.getPrototypeOf(obj)
    result = Object.create(proto)
   }
   for (let i in obj) {
    if(obj[i] && typeof(obj[i]) === 'object') {
     result[i] = deepCopy(obj[i], {
      originalParent: obj,
      currentParent: result,
      parent: parent
     })
    } else {
     result[i] = obj[i]
    }
   }
  }
 } else {
  return obj
 }
 return result
}
var obj1 = {
 x: 1 
}

//试调用
function construct(){
  this.a = 1,
  this.b = {
    x:2,
    y:3,
    z:[4,5,[6]]
  },
  this.c = [7,8,[9,10]],
  this.d = new Date(),
  this.e = /abc/ig,
  this.f = function(a,b){
    return a+b
  },
  this.g = null,
  this.h = undefined,
  this.i = "hello",
  this.j = Symbol("foo")
}
construct.prototype.str = "I'm prototype"
var obj1 = new construct()
obj1.k = obj1
obj2 = deepCopy(obj1)

obj2.b.x = 999
obj2.c[0] = 666

console.log('obj1', obj1)
console.log('obj2', obj2)

(3)函数库

也可以使用一些函数库,比如函数库lodash,也有提供_.cloneDeep用来做深拷贝;

var _ = require('lodash');
var obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

参考

以上就是详解JS深拷贝与浅拷贝的详细内容,更多关于JS深拷贝与浅拷贝的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
jQuery 遍历-nextUntil()方法以及prevUntil()方法的使用介绍
Apr 26 Javascript
jQuery列表拖动排列具体实现
Nov 04 Javascript
JavaScript splice()方法详解
Sep 22 Javascript
JavaScript利用append添加元素报错的解决方法
Jul 01 Javascript
java、javascript实现附件下载示例
Aug 14 Javascript
jQuery中triggerHandler()方法用法实例
Jan 19 Javascript
基于jQuery倾斜打开侧边栏菜单特效代码
Sep 15 Javascript
JavaScript 计算笛卡尔积实例详解
Dec 02 Javascript
使用BootStrap进行轮播图的制作
Jan 06 Javascript
jQuery Datatable 多个查询条件自定义提交事件(推荐)
Aug 24 jQuery
.vue文件 加scoped 样式不起作用的解决方法
May 28 Javascript
微信小程序中使用自定义图标(阿里icon)的方法
Aug 20 Javascript
vue addRoutes路由动态加载操作
Aug 04 #Javascript
vue+element使用动态加载路由方式实现三级菜单页面显示的操作
Aug 04 #Javascript
Vue登录拦截 登录后继续跳转指定页面的操作
Aug 04 #Javascript
vue 解决uglifyjs-webpack-plugin打包出现报错的问题
Aug 04 #Javascript
浅谈在vue-cli3项目中解决动态引入图片img404的问题
Aug 04 #Javascript
解决vuecli3中img src 的引入问题
Aug 04 #Javascript
简介JavaScript错误处理机制
Aug 04 #Javascript
You might like
PHP 在5.1.* 和5.2.*之间 PDO数据库操作中的不同之处小结
2012/03/07 PHP
php中禁止单个IP与ip段访问的代码小结
2012/07/04 PHP
php中的观察者模式简单实例
2015/01/20 PHP
php获取错误信息的方法
2015/07/17 PHP
PHP数组基本用法与知识点总结
2020/06/02 PHP
父窗口获取弹出子窗口文本框的值
2006/06/27 Javascript
JS是否可以跨文件同时控制多个iframe页面的应用技巧
2007/12/16 Javascript
利用404错误页面实现UrlRewrite的实现代码
2008/08/20 Javascript
javascript时区函数介绍
2012/09/14 Javascript
jQuery选择器中含有空格的使用示例及注意事项
2013/08/25 Javascript
JS 屏蔽键盘不可用与鼠标右键不可用的方法
2013/11/18 Javascript
JS获取月的最后一天与JS得到一个月份最大天数的实例代码
2013/12/16 Javascript
jquery预览图片实现鼠标放上去显示实际大小
2014/01/16 Javascript
JavaScript极简入门教程(一):基础篇
2014/10/25 Javascript
jQuery Form表单取值的方法
2017/01/11 Javascript
JSONP基础知识详解
2017/03/19 Javascript
JS实现利用两个队列表示一个栈的方法
2017/12/13 Javascript
深入理解nodejs搭建静态服务器(实现命令行)
2019/02/05 NodeJs
uni-app如何实现增量更新功能
2020/01/03 Javascript
详解Vue.js 响应接口
2020/07/04 Javascript
python实现H2O中的随机森林算法介绍及其项目实战
2019/08/29 Python
德国婴儿推车和儿童安全座椅商店:BABYSHOP
2016/09/01 全球购物
JBL加拿大官方商店:扬声器、耳机等
2020/10/23 全球购物
Prototype如何实现页面局部定时刷新
2013/08/06 面试题
企业军训感想
2014/02/07 职场文书
大学军训感言600字
2014/02/25 职场文书
党员自我剖析材料
2014/08/31 职场文书
关于教师节的广播稿
2014/09/10 职场文书
法人委托书范本
2014/09/15 职场文书
公安局负责人查摆问题及整改方案
2014/09/27 职场文书
2014年工程师工作总结
2014/11/25 职场文书
出国留学单位推荐信
2015/03/26 职场文书
大学学生会主席竞选稿
2015/11/19 职场文书
创业计划书之物流运送
2019/09/17 职场文书
导游词之香港-太平山顶
2019/10/18 职场文书
python字符串拼接.join()和拆分.split()详解
2021/11/23 Python