大家好,这里是前端小橙,本文章将根据内存在深度讲解深浅拷贝,会分别介绍内存和深浅拷贝
先来简单看一张图片
1、什么是深浅拷贝
首先先了解什么是深浅拷贝
- 浅拷贝
两个变量指向同一个内存地址, 例如上图中, 当a修改内存中的值时, b的值也会发生改变。 - 深拷贝
两个变量指向两个不同的内存地址, 例如上图中, 当a修改内存中的值时, b的值不会发生改变。
看完这两个概念的话是否会产生一些疑问, 不管有或者没有思考两个问题:
- 变量在内存中分别是如何存储的?
- 什么情况下两个变量会指向同一个地址?
如果是从 前端面试题过来的思考第三个问题?
- 为什么Object.assign有时是深拷贝有时是浅拷贝?
2、js的数据类型在内存中的存储
2.1、js的数据类型
- 基本数据类型
Number、String、Boolean、Null、Undefined、Symbol - 引用数据类型
Object、Array、Function
2.2、了解内存
学过c/c++的同学对内存应该非常的了解, 但是只学习前端的小伙伴可能对内存就相对生疏一些,接下来会从最基础讲解内存
我们现在所使用的编程语言不管是c/c++、Java、JS等都是高级语言,我们所编写的代码最终都会被编译成计算机能识别的机器语言(也就是二进制)来被计算机识别。当然这不是我们需要关心的事情,但是需要有一定的了解。
2.3、不同变量在内存中的存储
每一个变量都会进入内存, 大家应该都听说过堆栈, 每一个变量或语句的运行都会存储在堆或者栈, 堆和栈都是内存的一部分, 下面就来看看不同变量在堆栈中的存储
- 基本数据类型
ini
//基本数据类型都会存储在栈空间
let a = 1;
let b = "b";
let c = true;
let d = null;
let e = undefined;
代码运行都会进行入栈和出栈的操作, 这个我们不关心, 当我们定义变量的时候, 基本数据类型的变量会被存储在栈中, 入栈的方式都是从栈顶入栈,然后后来的就会把先来的往下压, 被称为压栈, 栈顶在上面还是在下面这不重要,画图本身就是为了更直观的理解问题, 而内存空间本身也不是这样的, 所以不要在这个地方纠结, 我们了解了基本数据类型在内存中的存储方式,接下来看看引用类型是如何存储的
- 引用数据类型
js
//引用数据类型都会存储在堆空间
let obj = {
name:"orange"
}
引用类型的值是进行单独存储的,和基本数据类型不同的是,
-
- 基本数据类型是直接存储值
-
- 引用类型是存储值的地址, 而值是单独存放在另一个内存空间中
如果不理解欢迎评论,可以多听听大家的建议, 认识了变量在内存中的存储后, 我们就正式开始在内存的角度讲讲深浅拷贝
3、通过内存来解释深浅拷贝
3.1、区分基本类型和引用类型
先来看一段代码
js
//基本数据类型
let a = 10;
let b = a;
a = 20;
console.log(b) // b == 10
从图中可以看出,通过赋值运算符(=)是直接将a的值(a在内存中的值)给了另一个变量b,因为基本数据类型是值存储,所以直接就是将10给了b, 所以可以看出, 修改a的值,b的值不会受到影响。那么再来看一段代码
js
//引用类型
let a = { name:"orange" };
let b = a;
a.name = "apple";
console.log(b.name) // b.name == "apple"
由于引用类型的值在内存中是单独存储的, 使用赋值运算符(=)会将值的地址赋值给变量, 这样会导致两个变量同时指向一个内存地址, 所以一个变量改变,另一个也会跟着改变。
总结:对于基本数据类型来说, 无论如何操作都会有新的内存地址, 所以都属于深拷贝, 或者深浅拷贝只对于引用类型而言,不考虑基本数据类型,了解一下就可以了。
3.2、Object.assign
对于引用类型来说Object.assign方法可以是深拷贝也可以是浅拷贝, 这时就需要区分情况了
- Object.assign深拷贝
使用:Object.assign({},obj)
ini
let a = { name:"orange" };
let b = Object.assign({}, a);
a.name = "apple";
console.log(b.name) // b.name == "orange"
Object.assign({},obj)会将obj中第一层的数据都开辟新的内存进行存储, 所以可以实现引用类型的深拷贝,为什么说是将第一层的数据存入新的内存呢? 我们继续往下看,
先知道一个概念:当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候(即有嵌套对象),此方法,在二级属性以后就是浅拷贝
- Object.assign浅拷贝
使用:Object.assign({},obj)
ini
let a = {
name:"orange",
like:{
foot:"orange",
}
};
let b = Object.assign({}, a);
a.name = "apple";
console.log(b.name) // b.name == "orange"
这时候将a的其中一个元素改为引用类型,这是a相当于有两层对象, 而上面的例子是单层的,可以拷贝代码看看,使用Object.assign拷贝的b会不会受a的影响, 或者哪些属性会受影响,然后再来看下图:
这是a多层级引用类型在内存中的存储, 当使用Object.assign进行拷贝时, 拷贝的是a对象的第一层数据, 而a中的like也是一个引用类型,所以是单独存放在另一个内存空间的, 所以like并没有被重新开辟一块空间进行存储。
a的第一层数据被拷贝了,所以修改a.name,不会影响b.name,因为他们已经指向不同的内存地址了,而修改like中的foot的时候,b还是会被影响, 说明第二层之后的值,并不会开辟新的内存空间,还是指向同一地址, 所以在多层级引用类型时,Object.assign就属于浅拷贝。
3.3、100%深拷贝
经过上面的讲解, 大家应该都熟悉了,数据类型在内存中的存储, 也从内存的方面对深浅拷贝有了进一步的了解, 最后一个, 无论是单层还是多层的引用类型, 都要有方法进行深拷贝, 那么这个方法就是JSON.parse和JSON.stringify
ini
let a = {
name:"orange",
like:{
foot:"orange",
}
};
let b = JSON.parse(JSON.stringify(a));
a.name = "apple";
console.log(b.name)
这个例子就不画图了, 原理很简单, 就是会将每一层的数据都进行新的内存开辟, 然后修改指向, 如果上面的都懂了, 这个也就很好理解啦。
总结 : 最后祝大家技术越来越好, 不好就来看看高级前端面试题吧