-
C++面试题之浅拷贝和深拷贝的区别
2018-02-01 12:15:06先考虑一种情况,对一个已知对象进行拷贝,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。 先看一个例子,有一个学生类,数据成员时学生的人数和名字:#...先考虑一种情况,对一个已知对象进行拷贝,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。
先看一个例子,有一个学生类,数据成员时学生的人数和名字:
#include <iostream> using namespace std; class Student { private: int num; char *name; public: Student(); ~Student(); }; Student::Student() { name = new char(20); cout << "Student" << endl; } Student::~Student() { cout << "~Student " << (int)name << endl; delete name; name = NULL; } int main() { {// 花括号让s1和s2变成局部对象,方便测试 Student s1; Student s2(s1);// 复制对象 } system("pause"); return 0; }
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!这是由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间。
所以,在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
添加了自己定义拷贝构造函数的例子:
#include <iostream> using namespace std; class Student { private: int num; char *name; public: Student(); ~Student(); Student(const Student &s);//拷贝构造函数,const防止对象被改变 }; Student::Student() { name = new char(20); cout << "Student" << endl; } Student::~Student() { cout << "~Student " << (int)name << endl; delete name; name = NULL; } Student::Student(const Student &s) { name = new char(20); memcpy(name, s.name, strlen(s.name)); cout << "copy Student" << endl; } int main() { {// 花括号让s1和s2变成局部对象,方便测试 Student s1; Student s2(s1);// 复制对象 } system("pause"); return 0; }
执行结果:调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内存不同。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
再说几句:
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:
1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。3.浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。
关于std::shared_ptr的原理和实现可参考:C++笔试题之smart pointer的实现
一个完整的自定义类实现可参考:C++笔试题之String类的实现
-
JavaScript专题(五)深浅拷贝
2020-08-12 18:53:12了解拷贝背后的过程,避免不必要的错误 1. 介绍浅拷贝 2. 介绍深拷贝 3. 实现浅拷贝 4. 实现深拷贝 Js专题系列之深浅拷贝,我们一起加油~JavaScript专题之深浅拷贝
了解拷贝背后的过程,避免不必要的错误,Js专题系列之深浅拷贝,我们一起加油~
目录
一、拷贝示例
当我们在操作数据之前,可能会遇到这样的情况:
- 会经常改动一组数据,但可能会用到原始数据
- 我需要两组一样的数据,但我不希望改动一个另一个随之改动
- 我需要对数据操作前后进行对比
- …
当我们遇到类似需要场景时,首先想到的就是拷贝它,殊不知拷贝也大有学问哦~
下面简单的例子,你是否觉得熟悉?
1.1 基本类型拷贝示例
var str = 'How are you'; var newStr = str; newStr = 10 console.log(str); // How are you console.log(newStr); // 10
大家都能想到,字符串是基本类型,它的值保存在栈中,在对它进行拷贝时,其实是为新变量开辟了新的空间。
str
和newStr
就好比两个一模一样的房间,布局一致却毫无关联。1.2 引用类型拷贝示例
var data = [1, 2, 3, 4, 5]; var newData = data; newData[0] = 100; console.log(data[0]); // 100 console.log(newData[0]); // 100
类似的代码段,但这次我们使用数组这个引用类型举例,你会发现修改赋值后的数据,原始数据也跟着改变了,这显然不满足我们的需要。本篇文章就来聊一聊引用数据拷贝的学问。
如果大家对Js的数据类型存在着疑问,不妨看看《JavaScript中的基本数据类型》
二、浅拷贝
拷贝的划分都是针对引用类型来讨论的,浅拷贝——顾名思义,浅拷贝就是“浅层拷贝”,实际上只做了表面功夫:
var arr = [1, 2, 3, 4]; var newArr = arr; console.log(arr, newArr); // [1,2,3,4] [1,2,3,4] newArr[0] = 100; console.log(arr, newArr) // [100,2,3,4] [100,2,3,4]
不发生事情(操作)还好,一旦对新数组进行了操作,两个变量中所保存的数据都会发生改变。
发生这类情况的原因也是因为
引用类型
的基本特性:- 存储在变量处的值是一个指针(point),指向存储对象的内存地址。赋值给新变量相当于配了一把新钥匙,房间并没有换。
数组中的slice和concat都会返回一个新数组,我们一起来试一下:
var arr = [1,2,3,4]; var res = arr.slice(); // 或者 res = arr.concat() res[0] = 100; console.log(arr); // [1,2,3,4]
这个问题这么快就解决了?虽然对这一层数据进行了这样的的处理后,确实解决了问题,但!
var arr = [ { age: 23 }, [1,2,3,4] ]; var newArr = arr.concat(); arr[0].age = 18; arr[1][0] = 100; console.log(arr) // [ {age: 18}, [100,2,3,4] ] console.log(newArr) // [ {age: 18}, [100,2,3,4] ]
果然事情没有那么简单,这也是因为数据类型的不同。
S 不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。
既然
浅拷贝
达不到我们的要求,本着效率的原则,我们找找有没有帮助我们实现深拷贝
的方法。三、深拷贝的方法?
数据的方法失败了,还有没有其他办法?我们需要实现真正意义上的拷贝出独立的数据。
3.1 JSON
这里我们利用JSON的两个方法,
JSON.stringify()
,JSON.parse()
来实现最简洁的深拷贝var arr = ['str', 1, true, [1, 2], {age: 23}] var newArr = JSON.parse( JSON.stringify(arr) ); newArr[3][0] = 100; console.log(arr); // ['str', 1, true, [1, 2], {age: 23}] console.log(newArr); // ['str', 1, true, [100, 2], {age: 23}]
这个方法应该是实现深拷贝最简洁的方法,但是,它仍然存在问题,我们先来看看刚才都做了些什么:
- 定义一个包含都过类型的数组
arr
- JSON.stringify(arr), 将一个 JavaScript 对象或值转换为
JSON 字符串
- JSON.parse(xxx), 方法用来解析JSON字符串,构造由字符串描述的
值或对象
理解:
我们可以理解为,将原始数据转换为
新字符串
,再通过新字符串
还原为一个新对象
,这中改变数据类型的方式,间接的绕过了拷贝对象引用的过程,也就谈不上影响原始数据。限制:
这种方式成立的根本就是保证数据在“中转”时的完整性,而
JSON.stringify()
将值转换为相应的JSON格式
时也有缺陷:- undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
- 函数、undefined 被单独转换时,会返回 undefined,
- 如JSON.stringify(function(){})
- JSON.stringify(undefined)
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- NaN 和 Infinity 格式的数值及 null 都会被当做 null。
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
所以当我们拷贝函数、undefined等
stringify
转换有问题的数据时,就会出错,我们在实际开发中也要结合实际情况使用。举一反三:
既然是通过改变数据类型来绕过拷贝引用这一过程,那么单纯的数组深拷贝是不是可以通过现有的几个API来实现呢?
var arr = [1,2,3]; var newArr = arr.toString().split(',').map(item => Number(item)) newArr[0] = 100; console.log(arr); // [1,2,3] console.log(newArr); // [100,2,3]
注意,此时仅能对包含纯数字的数组进行深拷贝,因为:
- toString无法正确的处理对象和函数
- Number无法处理 false、undefined等数据类型
但我愿称它为纯数字数组深拷贝!
3.2 Object.assign()
有的人会认为
Object.assign()
,可以做到深拷贝,我们来看一下var obj = {a: 1, b: { c: 2 } } var newObj = Object.assign({}, obj) newObj.a = 100; newObj.b.c = 200; console.log(obj); // {a: 1, b: { c: 200 } } console.log(newObj) // {a: 100, b: { c: 200 } }
神奇,第一层属性没有改变,但第二层却同步改变了,这是为什么呢?
因为 Object.assign()拷贝的是(可枚举)属性值。
假如源值是一个对象的引用,它仅仅会复制其引用值。MDN传送门
四、自己实现深浅拷贝
既然现有的方法无法实现深拷贝,不妨我们自己来实现一个吧~
4.1 浅拷贝
我们只需要将所有属性即其嵌套属性原封不动的复制给新变量一份即可,抛开现有的方法,我们应该怎么做呢?
var shallowCopy = function(obj) { if (typeof obj !== 'object') return; // 根据obj的类型判断是新建一个数组还是对象 var newObj = obj instanceof Array ? [] : {}; // 遍历obj,并且判断是obj的属性才拷贝 for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; }
我们只需要将所有属性的引用拷贝一份即可~
4.2 深拷贝
相信大家在实现深拷贝的时候都会想到递归,同样是判断属性值,但如果当前类型为
object
则证明需要继续递归,直到最后var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; }
我们用白话来解释一下
deepCopy
都做了什么const obj = [1, { a: 1, b: { name: '余光'} } ]; const resObj = deepCopy(obj);
- 读取
obj
,创建 第一个newObj- 判断类型为
[]
- key为
0
(for in
以任意顺序遍历,我们假定按正常循序遍历) - 判断不是引用类型,直接复制
- key为
1
- 判断是引用类型
- 进入递归,重新走了一遍刚才的流程,只不过读取的是
obj[1]
- 判断类型为
另外请注意递归的方式虽然可以深拷贝,但是在性能上肯定不如浅拷贝,大家还是需要结合实际情况来选择。
参考
写在最后
前端专项进阶系列的第五篇文章
,希望它能对大家有所帮助,如果大家有什么建议,可以在评论区留言,能帮到自己和大家就是我最大的动力!JavaScript专题系列:
- JavaScript中的变量提升与预编译,一起去发现Js华丽的暗箱操作(系列一)
- JavaScript专题之数组去重(系列二)
- JavaScript专题之数组防抖(系列三)
- JavaScript专题系列之节流(系列四)
- 本文
- 下期预告:如果准确判断一个对象的类型
另外推荐大家阅读一下我的《前端内功进阶系列》
关于我
- 花名:余光
- WX:j565017805
- 沉迷JS,水平有限,虚心学习中
其他沉淀
《专项系列》以及《内功系列》github传送门,点个star鼓励一下吧~
-
一篇文章彻底搞懂浅拷贝和深拷贝
2018-07-16 16:36:32强烈推荐30个原生JavaScript的demo,包括canvas时钟特效、自定义视频播放器、搜索栏快速匹配、fetch访问资源、console调试...浅谈深拷贝和浅拷贝 深拷贝和浅拷贝的区别 为什么要使用深拷贝? 深拷贝的要求程度...强烈推荐30个原生JavaScript的demo,包括canvas时钟特效、自定义视频播放器、搜索栏快速匹配、fetch访问资源、console调试技巧等,先fork后学习,详见点击打开链接,欢迎点赞~~~谢谢,共同进步学习!
由博主《前端初级工程师面试系列一JS基础》文章一JS变量类型引伸的考点,变量类型分为基本类型和引用类型,那么在变量拷贝赋值时,也是不一样的,分为浅拷贝和深拷贝,是面试中常考的知识点,也是实际开发中经常会用到的内容。
目录
- 浅谈深拷贝和浅拷贝
- 只对第一层级做拷贝
- 拷贝所有层级
- 存在大量深拷贝需求的代码——immutable提供的解决方案
- 补充知识点: obj==null和obj===null的区别、怎么判断数组类型、for..in和for...of及forEach的区别、for...in在遍历对象时的坑
正文
前言: 最开始意识到深拷贝的重要性是在我使用redux的时候(react + redux), redux的机制要求在reducer中必须返回一个新的对象,而不能对原来的对象做改动,事实上,当时我当然不会主动犯这个错误,但很多时候,一不小心可能就会修改了原来的对象,例如:var newObj = obj; newObj.xxx = xxx 实际上,这个时候newObj和obj两个引用指向的是同一个对象,我修改了newObj,实际上也就等同于修改了obj,这,就是我和深浅拷贝的第一次相遇。
深拷贝和浅拷贝的区别
1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用
2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
我们希望在改变新的数组(对象)的时候,不改变原数组(对象)
我们在使用深拷贝的时候,一定要弄清楚我们对深拷贝的要求程度:是仅“深”拷贝第一层级的对象属性或数组元素,还是递归拷贝所有层级的对象属性和数组元素?
改变任意一个新对象/数组中的属性/元素, 都不改变原对象/数组
只对第一层级做拷贝
深拷贝数组(只拷贝第一级数组元素)
- 直接遍历
var array = [1, 2, 3, 4]; function copy (array) { let newArray = [] for(let item of array) { newArray.push(item); } return newArray; } var copyArray = copy(array); copyArray[0] = 100; console.log(array); // [1, 2, 3, 4] console.log(copyArray); // [100, 2, 3, 4]
该方法不做解释(逃...)
2. slice()
var array = [1, 2, 3, 4]; var copyArray = array.slice(); copyArray[0] = 100; console.log(array); // [1, 2, 3, 4] console.log(copyArray); // [100, 2, 3, 4]
slice() 方法返回一个从已有的数组中截取一部分元素片段组成的新数组(不改变原来的数组!)
用法:array.slice(start,end) start表示是起始元素的下标, end表示的是终止元素的下标
当slice()不带任何参数的时候,默认返回一个长度和原数组相同的新数组
3. concat()
var array = [1, 2, 3, 4]; var copyArray = array.concat(); copyArray[0] = 100; console.log(array); // [1, 2, 3, 4] console.log(copyArray); // [100, 2, 3, 4]
concat() 方法用于连接两个或多个数组。( 该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。)
用法:array.concat(array1,array2,......,arrayN)
因为我们上面调用concat的时候没有带上参数,所以var copyArray = array.concat();实际上相当于var copyArray = array.concat([]);也即把返回数组和一个空数组合并后返回
但是,事情当然不会这么简单,我上面的标题是 “深拷贝数组(只拷贝第一级数组元素)”,这里说的意思是对于一级数组元素是基本类型变量(如number,String,boolean)的简单数组, 上面这三种拷贝方式都能成功,但对第一级数组元素是对象或者数组等引用类型变量的数组,上面的三种方式都将失效,例如:
var array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; var copyArray = array.slice(); copyArray[0].number = 100; console.log(array); // [{number: 100}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
深拷贝对象
1.直接遍历
var obj = { name: '彭湖湾', job: '学生' } function copy (obj) { let newObj = {}; for (let item in obj ){ newObj[item] = obj } return newObj; } var copyObj = copy(obj); copyObj.name = '我才不是彭湖湾呢! 哼 (。・`ω´・)'; console.log(obj); // {name: "彭湖湾", job: "学生"} console.log(copyObj); // {name: "我才不是彭湖湾呢! 哼 (。・`ω´・)", job: Object}
该方法不做解释(逃...)
2.ES6的Object.assign
var obj = { name: '彭湖湾', job: '学生' } var copyObj = Object.assign({}, obj); copyObj.name = '我才不叫彭湖湾呢! 哼 (。・`ω´・)'; console.log(obj); // {name: "彭湖湾", job: "学生"} console.log(copyObj); // {name: "我才不叫彭湖湾呢! 哼 (。・`ω´・)", job: "学生"}
Object.assign:用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),并返回合并后的target
用法: Object.assign(target, source1, source2); 所以 copyObj = Object.assign({}, obj); 这段代码将会把obj中的一级属性都拷贝到 {}中,然后将其返回赋给copyObj
3.ES6扩展运算符:
var obj = { name: '彭湖湾', job: '学生' } var copyObj = { ...obj } copyObj.name = '我才不叫彭湖湾呢! 哼 (。・`ω´・)' console.log(obj.name) // 彭湖湾 console.log(copyObj.name) // 我才不叫彭湖湾呢! 哼 (。・`ω´・)
扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
⚠️注意:实际上,无论是使用扩展运算符(...)还是解构赋值,对于引用类型都是浅拷贝。所以在使用splice()、concat()、...对数组拷贝时,只有当数组内部属性值不是引用类型是,才能实现深拷贝。对多层嵌套对象,也即是存在,很遗憾,上面三种方法,都会失败:
var obj = { name: { firstName: '彭', lastName: '湖湾' }, job: '学生' } var copyObj = Object.assign({}, obj) copyObj.name.lastName = '湖水的小浅湾'; console.log(obj.name.lastName); // 湖水的小浅湾 console.log(copyObj.name.lastName); // 湖水的小浅湾
拷贝所有层级
有没有更强大一些的解决方案呢?使得我们能够
1.不仅拷贝第一层级,还能够拷贝数组或对象所有层级的各项值
2. 不是单独针对数组或对象,而是能够通用于数组,对象和其他复杂的JSON形式的对象
请看下面:
下面这一招可谓是“一招鲜,吃遍天”
1.JSON.parse(JSON.stringify(XXXX))
var array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; var copyArray = JSON.parse(JSON.stringify(array)) copyArray[0].number = 100; console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
JSON.parse() 方法用于将一个 JSON 字符串转换为对象--(反序列化)
JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串--(序列化)
序列化的缺点:
- 不支持基本数据类型的undefined,序列化后将其省略
- 不支持函数
- Nan,Infinity序列化的结果是null
能用大招杀的就不要用q杀嘛!!
2.手动写递归
你说啥? 你说上面的那种方法太无脑, 一定要自己写一段递归才有做技术的感觉? OK成全你!
let array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; function copy (obj) { //首先判断需要拷贝的“东西”是什么类型 if(typeof obj !== 'object' || obj == null){ return; } let newobj = obj.constructor === Array ? [] : {}; //obj是数组类型,下面的i就是index;obj是对象,i就是key for(let i in obj){ newobj[i] = typeof obj[i] === 'object' ? copy(obj[i]) : obj[i]; } return newobj } let copyArray = copy(array) copyArray[0].number = 100; console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
【注意】上文的所有的示例都忽略了一些特殊的情况: 对对象/数组中的Function,正则表达式等特殊类型的拷贝
上述代码中还隐藏这两个知识点:
(1)obj == null 为何不是 obj === null?
obj==null =>obj为null 或者 obj 为undefined,因为null == undefined,这样写能默认处理两种情况,obj===null ,成立前提只有obj是null
(2)怎样判断一个对象是不是数组?
先上方法(确定的方法有两种)
- 根据对象的class属性来判断,跨原型链调用toString()方法。 Object. prototype.toString.call(obj)===[ object Array]
- Array.isArray直接判断 Array. isArray(obj)。
补充:
- 推荐文章:为什么用Object.prototype.toString.call(obj)检测对象类型
toString为Object的原型方法,返回一个用来描述该对象的字符串,所以可以调用对象原型方法toString()来探明对象的信息。
那么原型方法怎么调用呢,利用call,将其this指向需要判断的对象, 就可以用toString()方法了。let arr=[1,2,3]; console.log(Array.prototype.hasOwnProperty("toString"));//true console.log(arr.toString());//1,2,3 arr作为对象Object的实例,重写了toString()方法。 delete Array.prototype.toString;//delete操作符可以删除实例属性 console.log(Array.prototype.hasOwnProperty("toString"));//false console.log(arr.toString());//"[object Array]" // 由于删除了实例对象中的toString()方法,找不到,顺着原型链往上走,就调用了对象Object的方法,返回的结果就和Array.prototype.toString(arr)一样的。
- 根据构造函数来判断 instanceof 操作符可以来表示实例是否属于某个构造函数创建的。
这种方法有一个问题,就是验证不够严格。 即使对象创建时不是使用数组创建的,但是只要原型链上有数组类型,也认为是数组,亦或者,即便创建时是数组创建,但其原型上有对象类型,便不再被认为是数组。
(3)for...in 和for...of,forEach的区别,for...in用于对象复制时需要注意什么?
for... in特点
-
遍历对象返回的对象的key值,遍历数组返回的数组的下标(key)。
-
for ... in 会遍历原型上的属性值
-
遍历返回数据是乱序
总结一句: for in 循环特别适合遍历对象。
for... of特点
-
for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name
-
for ... in 会遍历原型上的属性值
-
遍历返回数据是乱序
-
for of 不同与 forEach, 它可以与 break、continue和return 配合使用,也就是说 for of 循环可以随时退出循环。
总结一句: for of 比较适合遍历数组,及其他具有遍历器的集合
forEach特点
-
使用foreach遍历数组的话,使用break不能中断循环,使用return也不能返回到外层函数。forEach与break和return 不搭
-
forEach()无法在所有元素都传递给调用的函数之前终止遍历
for…in循环可应用于对象的复制,不过其有一个缺点,就是会从原型属性里继承prototype()属性。
例如:
let array = [1,2,3,4,5] Array.prototype.age = 13; var result = []; for(let i in array){ result.push(array[i]); } alert(result.join(“,”)); result返回结果【1,2,3,4,5,13】
如何避免从原型属性里继承prototype()属性,这里使用hasOwnProperty(name),该函数指示一个对象自身(不包括原型链)是否具有指定名称的属性。如果有返回true,如果没有返回false。
let array = [1,2,3,4,5] Array.prototype.age = 13; var result = []; for(let i in array){ if(array.hasOwnProperty(i)){ result.push(array[i]); } alert(result.join(“,”)); } result返回结果【1,2,3,4,5】
所以上面的深拷贝代码应优化为如下:
let array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; function copy (obj) { //首先判断需要拷贝的“东西”是什么类型 if(typeof obj !== 'object' || obj == null){ return; } let newobj = Array.isArray(obj) ? [] : {}; //obj是数组类型,下面的i就是index;obj是对象,i就是key for(let i in obj){ if(obj.hasOwnProperty(i)){ newobj[i] = typeof obj[i] === 'object' ? copy(obj[i]) : obj[i]; } } return newobj } let copyArray = copy(array) copyArray[0].number = 100; console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
存在大量深拷贝需求的代码——immutable提供的解决方案
实际上,即使我们知道了如何在各种情况下进行深拷贝,我们也仍然面临一些问题: 深拷贝实际上是很消耗性能的。(我们可能只是希望改变新数组里的其中一个元素的时候不影响原数组,但却被迫要把整个原数组都拷贝一遍,这不是一种浪费吗?)所以,当你的项目里有大量深拷贝需求的时候,性能就可能形成了一个制约的瓶颈了。
immutable的作用:
通过immutable引入的一套API,实现:
1.在改变新的数组(对象)的时候,不改变原数组(对象)
2.在大量深拷贝操作中显著地减少性能消耗
先睹为快:
const { Map } = require('immutable') const map1 = Map({ a: 1, b: 2, c: 3 }) const map2 = map1.set('b', 50) map1.get('b') // 2 map2.get('b') // 50
-
Java 深拷贝 和 浅拷贝 clone
2019-11-27 16:15:22关于Java的深拷贝和浅拷贝,简单来说就是创建一个和已知对象一模一样的对象。可能日常编码过程中用的不多,但是这是一个面试经常会问的问题,而且了解深拷贝和浅拷贝的原理,对于Java中的所谓值传递或者引用传递将会...关于Java的深拷贝和浅拷贝,简单来说就是创建一个和已知对象一模一样的对象。可能日常编码过程中用的不多,但是这是一个面试经常会问的问题,而且了解深拷贝和浅拷贝的原理,对于Java中的所谓值传递或者引用传递将会有更深的理解。
1、创建对象的5种方式
- 通过 new 关键字
这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object();
- 通过 Class 类的 newInstance() 方法
这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class. forName(“com.ys.test. Person”). newInstance();
- 通过 Constructor 类的 newInstance 方法
这和第二种方法类时,都是通过反射来实现。通过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来创建对象。
Person p3 = (Person) Person.class.getConstructors()[0].newInstance();
实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是 Constructor 的 newInstance() 方法。 - 利用 Clone 方法
Clone 是 Object 类中的一个方法,通过 对象A.clone() 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。
Person p4 = (Person) p3.clone(); - 序列化
序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。序列化
Java 基本复制方法
java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的:
修改新对象的值会同时修改旧对象的值。public class Client { public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode()); System.out.println(person.hashCode()); System.out.println("================"); System.out.println(p1.display()); System.out.println(person.display()); } }
Clone 方法
如果创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,而互不影响,就需要用到java中对象的复制,如原生的clone()方法。
本次讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:/** * ... * performs a "shallow copy" of this object, not a "deep copy" operation. * 上面这里已经说明了,clone()方法是浅拷贝,而不是深拷贝 * @see java.lang.Cloneable */ protected native Object clone() throws CloneNotSupportedException;
这是一个用 native 关键字修饰的方法,关于native关键字有一篇博客专门有介绍,不理解也没关系,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现(参考JNI)。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是什么关系呢?
基本类型和引用类型
这里再给大家普及一个概念,在 Java 中基本类型和引用类型的区别。在 Java 中数据类型可以分为两大类:基本类型和引用类型。
基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。
引用类型则包括类、接口、数组、枚举等。
Java 将内存空间分为堆和栈。基本类型直接在栈 stack中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆 heap中,通过栈中的引用指向堆中存放的数据。
上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在栈中,然后指向堆的内存空间。
下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。浅拷贝
接下来用代码看看浅拷贝的效果。
package mytest; @Data//lombok注解 class Person implements Cloneable { private int age; private String name; private Address address; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; } } @Data//lombok注解 class Address { private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; } } public class Client { public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person clonePerson = (Person) person.clone(); System.out.println(person); System.out.println(clonePerson); // 信息完全一样 System.out.println(person.display()); System.out.println(clonePerson.display()); System.out.println("信息完全一致"); System.out.println("原始年龄:" + person.getAge()); System.out.println("克隆后原始年龄:" + clonePerson.getAge()); System.out.println("年龄完全一样"); System.out.println("原始名字哈希值:" + person.getName().hashCode()); System.out.println("克隆后名字哈希值:" + clonePerson.getName().hashCode()); System.out.println("字符串哈希值完全一样"); clonePerson.setName("xiaomai"); clonePerson.setAge(20); clonePerson.getAddress().setStreet("天府四街"); System.out.println(clonePerson.display()); System.out.println(person.display()); System.out.println("年龄跟姓名 是完全的深拷贝 副本跟原值无关的!"); System.out.println("地址信息的修改是浅拷贝 "); } }
结果如下:
mytest.Person@15f550a
mytest.Person@6b2d4a
Person [age=15, name=sowhat, address=Address [province=河北, street=建华南大街]]
Person [age=15, name=sowhat, address=Address [province=河北, street=建华南大街]]
信息完全一致
原始年龄:15
克隆后原始年龄:15
年龄完全一样
原始名字哈希值:-1432601412
克隆后名字哈希值:-1432601412
字符串哈希值完全一样
Person [age=20, name=xiaomai, address=Address [province=河北, street=中山路]]
Person [age=15, name=sowhat, address=Address [province=河北, street=中山路]]结论:
- 原对象与新对象是两个不同的对象。
- 拷贝出来的新对象与原对象内容一致
- 接着将新对象里面的信息进行了修改,然后输出发现原对象里面的部分信息也跟着变了。其中 基本类型跟 String类型的改变不会影响到 原始对象的改变。而其他的Ojbect 类型改变的时候会影响到原始数据。
上面的结论称为浅拷贝
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该对象,如果字段类型是值类型(基本类型跟String)的,那么对该字段进行复制;如果字段是引用类型的,则只复制该字段的引用而不复制引用指向的对象(也就是只复制对象的地址)。此时新对象里面的引用类型字段相当于是原始对象里面引用类型字段的一个副本,原始对象与新对象里面的引用字段指向的是同一个对象
。
因此,修改clonePerson里面的address内容时,原person里面的address内容会跟着改变。
深拷贝
了解了浅拷贝,那么深拷贝是什么也就很清楚了。那么该如何实现深拷贝呢?
Object 类提供的 clone 是只能实现 浅拷贝的
。,即将引用类型的属性内容也拷贝一份新的。那么,实现深拷贝我这里收集到两种方式:
第一种是给需要拷贝的引用类型也实现Cloneable接口并覆写clone方法;
第二种则是利用序列化。接下来分别对两种方式进行演示。深拷贝-clone方式
对于以上演示代码,利用clone方式进行深拷贝无非就是将Address类也实现Cloneable,然后对Person的clone方法进行调整。让每个引用类型属性内部都重写clone() 方法,既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),我们在 Address 类内部也重写 clone 方法。如下:
package mytest; @Data//lombok注解 class Person implements Cloneable { private int age; private String name; private Address address; protected int abc = 12; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } @Override // clone 重载 protected Object clone() throws CloneNotSupportedException { Person person = (Person) super.clone(); //手动对address属性进行clone,并赋值给新的person对象 person.address = (Address) address.clone(); return person; } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; } } @Data//lombok注解 class Address implements Cloneable { private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } // 深拷贝时添加 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; } } public class Client { public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode()); System.out.println(person.hashCode()); System.out.println(p1.display()); System.out.println(person.display()); System.out.println("-----------"); Person clonePerson = (Person) person.clone(); System.out.println(person); System.out.println(clonePerson); // 信息完全一样 System.out.println(person.display()); System.out.println(clonePerson.display()); System.out.println("信息完全一致"); System.out.println("原始年龄:" + person.getAge()); System.out.println("克隆后原始年龄:" + clonePerson.getAge()); System.out.println("年龄完全一样"); System.out.println("原始名字哈希值:" + person.getName().hashCode()); System.out.println("克隆后名字哈希值:" + clonePerson.getName().hashCode()); System.out.println("字符串哈希值完全一样"); clonePerson.setName("xiaomai"); clonePerson.setAge(20); clonePerson.getAddress().setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); System.out.println("年龄跟姓名 是完全的深拷贝 副本跟原值无关的!"); System.out.println("地址信息的修改是浅拷贝 "); } }
但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone 方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。
利用序列化
序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。
注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。package mytest; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 利用序列化和反序列化进行对象的深拷贝 * @author ljj */ class DeepClone implements Serializable { private static final long serialVersionUID = 1412L; public Object deepClone() throws Exception { //序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } } @Data class Person extends DeepClone { private static final long serialVersionUID = 1L; private int age; private String name; private Address address; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; } } @Data class Address extends DeepClone { private static final long serialVersionUID = 1412L; private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; } public void setStreet(String street) { this.street = street; } } public class Client { public static void main(String[] args) throws Exception { Person person = new Person(15, "sowhat", new Address("河北", "建华南大街")); Person clonePerson = (Person) person.deepClone(); System.out.println(person); System.out.println(clonePerson); System.out.println(person.display()); System.out.println(clonePerson.display()); clonePerson.setName("xiaomai"); clonePerson.setAge(20); Address address = clonePerson.getAddress(); address.setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); } }
- 通过 new 关键字
-
深拷贝和浅拷贝
2019-05-12 17:28:52深拷贝和浅拷贝的知识涉及到堆栈的概念。 堆栈的概念: 基本类型: 名值存储在栈内存中,例如: let a = 1; 引用类型: 名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值; 浅... -
C++拷贝构造函数详解
2011-02-23 13:39:00什么是拷贝构造函数 首先对于普通类型的对象来说,它们之间的复制是很简单的,例如: int a = 100; int b = a; 而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。 下面看一个类对象... -
BeanUtils.copyProperties的使用(深拷贝,浅拷贝)
2020-03-18 23:30:05这里说的是spring的BeanUtils.copyProperties。 场景 开发中经常遇到,把父类的属性拷贝到子类中。通常有2种方法: 1、一个一个set ...是深拷贝,还是浅拷贝? 是浅拷贝。 浅拷贝: 只是调用子对象的set... -
浅拷贝与深拷贝
2021-02-11 11:54:43引用拷贝 A a=new A(); A b=a; 以上是 引用拷贝 a和b 同时引用了堆内存上的new A()操作; .对象拷贝 对对象进行拷贝,拷贝完的对象不会同时引用一个堆内存上的数据。若A是源对象 B是拷贝后的对象 则A==B操作返回... -
基础面试题:深拷贝和浅拷贝详解以及实例
2020-04-30 12:59:55深拷贝 vs 浅拷贝 浅拷贝 概念 复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。 如图: 特点 1.对于基本数据类型的成员对象,因为基础... -
Java深入理解深拷贝和浅拷贝区别
2019-02-13 23:31:47一、拷贝的引入 (1)、引用拷贝 创建一个指向对象的引用变量的拷贝。 Teacher teacher = new Teacher("Taylor",26); Teacher otherteacher = teacher; System.out.println(teacher); System.out.println... -
-
C++细节 深拷贝和浅拷贝(位拷贝)详解
2018-08-07 21:00:14在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。 阅读《高质量的... -
Golang深拷贝浅拷贝
2019-05-30 23:19:37Golang深拷贝浅拷贝 在了解原型设计模式之前我们需要新知道Golang的深拷贝与浅拷贝之间的区别。 推荐大家新看看Slice 和 Map那么常见的坑:https://blog.csdn.net/weixin_40165163/article/details/90707593 ... -
深拷贝和浅拷贝区别是什么?
2019-06-18 15:44:47深拷贝和浅拷贝区别是什么? 复制一个 Java 对象 浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。 深拷贝:复制基本类型的属性;... -
c++的默认拷贝构造函数,从深度拷贝和浅拷贝说起
2017-07-24 19:44:101. c++类的默认拷贝构造函数的弊端c++类的中有两个特殊的构造函数,(1)无参构造函数,(2)拷贝构造函数。它们的特殊之处在于: (1)当类中没有定义任何构造函数时,编译器会默认提供一个无参构造函数且其函数体为空;... -
ECMAScript中的浅拷贝和深拷贝
2020-02-07 19:09:27一.什么是浅拷贝,什么是深拷贝 浅拷贝:浅拷贝是将原始对象中...深拷贝:深拷贝:深拷贝是在引用方面不同,深拷贝就是创建一个新的和原始字段的内容相同的字段,是两个一样大的数据段,所以两者的引用是不同的,之后... -
终于弄清楚JS的深拷贝和浅拷贝了-读这一篇就够了
2018-07-27 17:22:14今天,CVTE面试官问了深拷贝和浅拷贝的问题 我的回答是:浅拷贝是拷贝了对象的引用,当原对象发生变化的时候,拷贝对象也跟着变化;深拷贝是另外申请了一块内存,内容和原对象一样,更改原对象,拷贝对象不会发生... -
python浅拷贝和深度拷贝
2019-07-05 09:36:21通过代码和结果来理解浅拷贝和深度拷贝,先来看看python中有那些常见的浅拷贝方法: 1.使用数据类型本身的构造器: l1 = [1,2,3] l2 = list(l1) l1 == l2 #True l1 is l2 #False #l2就是l1的浅拷贝,set、dict与... -
深拷贝与浅拷贝
2020-06-28 17:04:11无论是深拷贝还是浅拷贝,都需要实现 Cloneable 接口,并覆写 clone() 方法。 浅拷贝 @Override public Object clone() { //浅拷贝 try { // 直接调用父类的clone()方法 return super.clone(); } catch ... -
python:深拷贝,浅拷贝,赋值引用
2019-05-23 21:08:151. python的复制,深拷贝和浅拷贝的区别 在python中,对象赋值实际上是对象的引用。当创建一个对象,然后把它赋给另一个变量的时候,python并没有拷贝这个对象,而只是拷贝了这个对象的引用 一般有三种方法, a... -
java深拷贝和浅拷贝
2020-02-11 15:20:58图解深拷贝和浅拷贝的差别 基本数据类型 引用数据类型 浅拷贝 拷贝值 拷贝引用 深拷贝 拷贝值 拷贝引用对象 深拷贝的实现方式 1:手动赋值(子对象也要重新复制)在对象内 写一个copy方法。 2:使用... -
JAVA中的深拷贝与浅拷贝以及引用拷贝
2020-06-18 21:24:59文章目录JAVA中的深拷贝与浅拷贝以及引用拷贝背景浅拷贝含义实例结果深拷贝含义结果引用拷贝结果 背景 最近在看JDK源码,看到集合的时候,大部分集合都继承了Cloneable,并且集合中的clone()调用Cloneable里面的... -
js浅拷贝和深拷贝给我点!!!!!
2020-03-05 22:31:30浅拷贝,前后会互相影响,深拷贝,前后结果完全是两个独立的对象、数组巴拉巴拉的,看你拷贝的是啥。 2.假如对象是一个筐,我手里有根绳子拉上来能拿到筐里的鸡蛋,浅拷贝就相当于你又拿根绳子绑到筐上了,咱俩拿到... -
拷贝加指针拷贝
2016-12-08 17:25:54对实例的构造就是分配一部分内存,而不对该部分内存做任何...今天早上读《effective c++》的第五章条款,想到的深拷贝、浅拷贝,对应值拷贝和位拷贝。 值拷贝和位拷贝的内容如下: http://blog.csdn.net/l -
深拷贝和浅拷贝实现方式
2018-05-05 15:37:21深拷贝 JSON方法实现 //_tmp和result是相互独立的,没有任何联系,有各自的存储空间。 let deepClone = function (obj) { let _tmp = JSON.stringify(obj);//将对象转换为json字符串形式 let result = JSON.parse(_... -
JavaScript对象浅拷贝和深拷贝
2020-12-03 15:51:32JavaScript对象浅拷贝和深拷贝1. 基本概念2. 浅拷贝3. 深拷贝3.1 深拷贝JSON方法3.2 深拷贝手写递归方法4, 1. 基本概念 对象是引用类型,在声明引用类型的时候,对象是存储在堆内存中的,而js不能直接操作内存,... -
深度拷贝与浅度拷贝
2018-07-25 22:56:52深度拷贝与浅度拷贝的区别主要在于有没有为拷贝出的新对象在堆中重新分配一块内存区域。浅度拷贝即直接赋值,拷贝的只是原始对象的引用地址,在堆中仍然共用一块内存。而深度拷贝为新对象在堆中重新分配一块内存,... -
Python的 深拷贝、浅拷贝
2020-05-31 17:22:50深拷贝copy.deepcopy( )和浅拷贝 copy.copy( )之间的区别是什么? 对象的赋值就是简单的引用,a = [1,2,3], b=a, 在上述情况下,a和b是一样的,他们指向同一片内存,b不过是a的别名,是引用,我们可以使用b is a 去判断,...
-
潍柴欧Ⅲ电控发动机CAN总线通讯技术应用规范V1.31.pdf
-
【硬核】一线Python程序员实战经验分享(1)
-
vue3.0 学习笔记
-
godot-scene-map:提供一个帮助节点,用于从大小相似的场景中构建基于图块的世界-源码
-
Amoeba 实现 MySQL 高可用、负载均衡和读写分离
-
淘宝补单的一些见解和经验分享,仅供参考
-
学校-源码
-
Java反射
-
双循环经济背景下的城镇化与新机遇 陆铭.pdf
-
SunloginClient向日葵官方最新包win64位安装包
-
libFuzzer视频教程
-
NL图编码检测器-源码
-
【每日笔记】:layui select获取自定义属性方法
-
自动化测试Python3+Selenium3+Unittest
-
四川理工学院《离散数学》期末考试试卷(含答案).pdf
-
golang sync.Map的使用
-
朱老师c++课程第3部分-3.5STL的其他容器讲解
-
重庆大学《操作系统》期末试卷(含答案).pdf
-
oracle基础教学:如何设置外键
-
JavaScript03,数据类型快速游览,严格检查模式 strict,字符串类型详解