【深浅拷贝原理1】解析赋值、浅拷贝和深拷贝的区别
堆和栈
堆和栈都是内存中划分出来用来存储的区域。
栈(stack)为自动分配的内存空间,它由系统自动释放;
堆(heap)则是动态分配的内存,大小不定也不会自动释放。
数据类型
JavaScript 分为两种数据类型
- 基本数据类型
- Number
- String
- Boolean
- null
- undefind
- 引用类型
- Object
- Function
- Array
基本数据类型
基本数据类型有以下特点:
- 基本数据类型存放在堆中
存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。
- 基本数据类型值不可变
javascript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个
基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的:
1 | var str = "abc"; |
- 基本类型的比较是值的比较
基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的。1
2
3var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc
比较的时候最好使用严格等,因为 == 是会进行类型转换的,比如:1
2
3var a = 1;
var b = true;
console.log(a == b);//true
引用类型
- 引用类型存放在堆中
引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配。
- 引用类型值可变
引用类型是可以直接改变其值的
1 | var a = [1,2,3]; |
- 引用类型的比较是引用的比较
每次我们对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。
1 | var a = [1,2,3]; |
虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。
传值与传址
进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中。1
2
3
4
5
6var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10
引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。
1 | var a = {}; // a保存了一个空对象的实例 |
赋值(Copy)
赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分
- 基本数据类型:赋值,赋值之后两个变量互不影响
- 引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响
浅拷贝(Shallow Copy)
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。
浅拷贝使用场景
Object.assign()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27let a = {
name: "xiaoming",
parents: {
father: "daming",
mother: "dahua"
}
}
let b = Object.assign({}, a);
console.log(b);
// {
// name: "xiaoming",
// book: {title: "daming", price: "dahua"}
// }
b.name = "zhou";
b.parents.mother = "niuniu";
console.log(a);
// {
// name: "xiaoming",
// book: {title: "daming", price: "niuniu"}
// }
console.log(b);
// {
// name: "zhou",
// book: {title: "daming", price: "niuniu"}
// }展开语法
Spread
1 | var obj1 = { |
展开语法和 Object.assign()
行为一致, 执行的都是浅拷贝(只遍历一层)。
- Array.prototype.slice()
slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
1 | let a = [0, "1", [2, 3]]; |
说明 slice()
方法是浅拷贝,相应的还有 concat
等,在工作中面对复杂数组结构要额外注意。
深拷贝(Deep Copy)
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
JSON.parse(JSON.stringify(object))
1 | let a = { |
但是该方法有以下几个问题。
- 会忽略
undefined
- 会忽略
symbol
- 不能序列化函数
- 能解决循环引用的对象
- 不能正确处理
new Date()
- 不能处理正则
undefined
、symbol
和函数这三种情况,会直接忽略。
除了上面介绍的深拷贝方法,常用的还有jQuery.extend()
和 lodash.cloneDeep()
.
总结
– | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 第一层数据为基本数据类型 | |
---|---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 | |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 | |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
赋值和浅拷贝的区别
对于基本数据类型,赋值和浅拷贝并无区别。
但是对于引用类型,两者有所区别。
1 | var obj1 = { |
obj2
是赋值的对象。obj3
是浅拷贝的对象。
被赋值的变量(obj2)和源数据指向同一个对象。即使修改的是引用类型中的属性是基本数据类型,源数据也会被改变。因为他们指向的是同一对象。
而浅拷贝只复制一层对象的属性,并不包括对象里面的引用类型的数据。
参考
【进阶4-1期】详细解析赋值、浅拷贝和深拷贝的区别
深拷贝与浅拷贝的实现(一)
js 深拷贝 vs 浅拷贝
展开语法 - JavaScript | MDN_