前言:在日常的开发中,我们常常遇到一些我们不懂的知识,比如我最近看到一个Object.assign() ,就不太清楚其究竟代表着啥意思,因而在查阅资料后,得知其是前端浅拷贝的一种方式。以前也有听说过深拷贝,但不太清楚其代表的意思。因此,今天借此篇博文,来看看什么是浅拷贝,什么又是深拷贝,它们又是如何在开发中运用的。
1,浅拷贝和深拷贝的概念
1.1 浅拷贝和深拷贝
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,A2 中依然包含对B1的引用,B1中依然包含对C1的引用。深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。
为了便于对于浅拷贝和深拷贝的记忆,我们引入引用拷贝和对象拷贝来加深其理解:
1.2 引用拷贝和对象拷贝
引用拷贝:创建一个指向对象的引用变量的拷贝,具体代码如下:
public class QuoteCopy {
public static void main(String[] args) {
Teacher teacher = new Teacher("riemann", 28);
Teacher otherTeacher = teacher;
System.out.println(teacher);
System.out.println(otherTeacher);
}
}
class Teacher {
private String name;
private int age;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
输出结果:
com.test.Teacher@28a418fc
com.test.Teacher@28a418fc
结果分析:由输出结果可以看出,它们的地址值是相同的,那么它们肯定是同一个对象。teacher和otherTeacher的只是引用而已,他们都指向了一个相同的对象Teacher(“riemann”,28)。 这就叫做引用拷贝。
对象拷贝:创建对象本身的一个副本,代码如下:
public class ObjectCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("riemann", 28);
Teacher otherTeacher = (Teacher) teacher.clone();
System.out.println(teacher);
System.out.println(otherTeacher);
}
}
class Teacher implements Cloneable {
private String name;
private int age;public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Object clone() throws CloneNotSupportedException {
Object object = super.clone();
return object;
}
}
输出结果:
com.test.Teacher@28a418fc
com.test.Teacher@5305068a
结果分析:由输出结果可以看出,它们的地址是不同的,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量,这就叫做对象拷贝。
1.3 浅拷贝和深拷贝的实例
深拷贝和浅拷贝都是对象拷贝,其各有特点,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象,因而相对开销较小;深拷贝把要复制的对象所引用的对象都复制了一遍,速度较慢并且花销较大。在实际的应用中,我们对于浅拷贝的方法用的较多,比如常见的clone()方法,其拷贝出来的对象是通过浅拷贝,若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝,具体代码示例如下:
我们可以通过创建一个学生对象,里边引入教授来区别浅拷贝和深拷贝:
//浅拷贝内部对象——教授
class Professor0 implements Cloneable {
String name;
int age;
Professor0(String name, int age) {
this.name = name;
this.age = age;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//浅拷贝克隆和修改对象——学生
class Student0 implements Cloneable {
String name;// 常量对象。
int age;
Professor0 p;// 学生1和学生2的引用值都是一样的。
Student0(String name, int age, Professor0 p) {
this.name = name;
this.age = age;
this.p = p;
}
public Object clone() {
Student0 o = null;
try {
o = (Student0) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
return o;
}
}
通过固定方法调用查看区别:
public class ShallowCopy {
public static void main(String[] args) {
Professor0 p = new Professor0("wangwu", 50);
Student0 s1 = new Student0("zhangsan", 18, p);
Student0 s2 = (Student0) s1.clone();
s2.p.name = "lisi";
s2.p.age = 30;
s2.name = "z";
s2.age = 45;
System.out.println("学生s1的姓名:" + s1.name +
"\n学生s1教授的姓名:" + s1.p.name + "," +
"\n学生s1教授的年纪" + s1.p.age);// 学生1的教授
}
}
结果:s2变了,但s1也变了
证明:s1的p和s2的p指向的是同一个对象。
这在我们有的实际需求中,却不是这样,因而我们需要深拷贝:
//深拷贝内部对象——教授
class Professor implements Cloneable {
String name;
int age;
Professor(String name, int age) {
this.name = name;
this.age = age;
}
public Object clone() {
Object o = null;
try {
o = super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
return o;
}
}
//浅拷贝克隆对象——学生
class Student implements Cloneable {
String name;
int age;
Professor p;
Student(String name, int age, Professor p) {
this.name = name;
this.age = age;
this.p = p;
}
public Object clone() {
Student o = null;
try {
o = (Student) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
o.p = (Professor) p.clone();
return o;
}
}
深拷贝方法修改对象:
public class DeepCopy {
public static void main(String args[]) {
long t1 = System.currentTimeMillis();
Professor p = new Professor("wangwu", 50);
Student s1 = new Student("zhangsan", 18, p);
Student s2 = (Student) s1.clone();
s2.p.name = "lisi";
s2.p.age = 30;
System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);// 学生1的教授不改变。
long t2 = System.currentTimeMillis();
System.out.println(t2-t1);
}
}
当然我们还有一种深拷贝方法,就是将对象串行化:
import java.io.*;
class Professor2 implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;Professor2(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student2 implements Serializable {
private static final long serialVersionUID = 1L;
String name;// 常量对象。
int age;
Professor2 p;// 学生1和学生2的引用值都是一样的。
Student2(String name, int age, Professor2 p) {
this.name = name;
this.age = age;
this.p = p;
}
public Object deepClone() throws IOException, OptionalDataException,
ClassNotFoundException {
// 将对象写到流里
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(this);
// 从流里读出来
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return (oi.readObject());
}
}
public class DeepCopy2 {
public static void main(String[] args) throws OptionalDataException,
IOException, ClassNotFoundException {
long t1 = System.currentTimeMillis();
Professor2 p = new Professor2("wangwu", 50);
Student2 s1 = new Student2("zhangsan", 18, p);
Student2 s2 = (Student2) s1.deepClone();
s2.p.name = "lisi";
s2.p.age = 30;
System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age); // 学生1的教授不改变。
long t2 = System.currentTimeMillis();
System.out.println(t2-t1);
}
}
参考资料:(144条消息) Java深入理解深拷贝和浅拷贝区别_老周聊架构的博客-CSDN博客_java深拷贝和浅拷贝的区别
2.浅拷贝和深拷贝的适用场景
2.1 浅拷贝和深拷贝的前端应用
浅拷贝和深拷贝都是只针对于像Object,Array这样的复杂对象,经常用于前端js对象的封装和改变。其两者的区别在于——浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制,
浅拷贝就是增加了一个指针指向已存在的内存(JavaScript并没有指针的概念,这里只是用于辅助说明),浅拷贝只是拷贝了内存地址,子类的属性指向的是父类属性的内存地址,当子类的属性修改后,父类的属性也随之被修改。就如同:一件衣服两个人穿不管你穿还是我穿都还是同一件衣服。
深拷贝就是增加一个指针,并申请一个新的内存,并且让这个新增加的指针指向这个新的内存地址使用深拷贝,在释放内存的时候就不会像浅拷贝一样出现释放同一段内存的错误,当我们需要复制原对象但有不能修改原对象的时候,深拷贝就是一个,也是唯一的选择,就如同买不同的新衣服。像es6的新增方法都是深拷贝,所以推荐使用es6语法。
2.2浅拷贝和深拷贝的使用实例
浅拷贝:
1.Object.assign:Object.assign 是 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
2、扩展运算符:使用扩展运算符也可以完成浅拷贝
3、Array.prototype.concat:数组的 concat 方法其实也是浅拷贝,使用场景比较少,使用concat连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组
4、Array.prototype.slice
数组的 slice 方法其实也是浅拷贝,使用场景比较少,同cancat
5、使用第三方库&手动实现
社区存在一些优秀工具函数库,在开发过程中可以直接利用这些库暴露的函数直接实现浅拷贝,比如lodash就提供了clone方法供用户进行浅拷贝
深拷贝:
1.可以通过 for in 实现。
function deepCopy1(obj) {
let o = {}
for(let key in obj) {
o[key] = obj[key]
}
return o
}
let obj = {
a:1,
b: undefined,
c:function() {},
}
console.log(deepCopy1(obj))
2. 可以借用JSON对象的parse和stringify(对象的深拷贝)
function deepClone(obj){
let _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone
}
let a=[0,1,[2,3],4],
b=deepClone(a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
3.递归 自身调用自身(对象的深拷贝)
function deepClone1(obj) {
//判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
var objClone = Array.isArray(obj) ? [] : {};
//进行深拷贝的不能为空,并且是对象或者是
if (obj && typeof obj === "object") {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepClone1(obj[key]);
} else {
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
4.concat(数组的深拷贝)
使用concat合并数组,会返回一个新的数组。
5.有些同学可能见过用系统自带的JSON来做深拷贝的例子,下面来看下代码实现
function cloneJSON(source) {
return JSON.parse(JSON.stringify(source));
}
2.3 浅拷贝与深拷贝的关系(待更新)
浅拷贝与深拷贝是一道经久不衰的面试题。
我们要答好这道题,就必须先理清其关系,简单来说,深拷贝可以视为浅拷贝+递归。
深拷贝、浅拷贝的理解与使用场景 - 简书 (jianshu.com)
https://blog.csdn.net/weixin_50964668/article/details/113922558