【深浅拷贝原理1】解析赋值、浅拷贝和深拷贝的区别

Author Avatar
Klein 3月 19, 2019

堆和栈

堆和栈都是内存中划分出来用来存储的区域。

栈(stack)为自动分配的内存空间,它由系统自动释放;
堆(heap)则是动态分配的内存,大小不定也不会自动释放。

数据类型

JavaScript 分为两种数据类型

  • 基本数据类型
    • Number
    • String
    • Boolean
    • null
    • undefind
  • 引用类型
    • Object
    • Function
    • Array

基本数据类型

基本数据类型有以下特点:

  1. 基本数据类型存放在堆中

存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。

  1. 基本数据类型值不可变

    javascript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个

基本数据类型的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的:

1
2
3
var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc
  1. 基本类型的比较是值的比较

基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的。

1
2
3
var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc

比较的时候最好使用严格等,因为 == 是会进行类型转换的,比如:

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

引用类型

  1. 引用类型存放在堆中

引用类型(object)是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配。

  1. 引用类型值可变

引用类型是可以直接改变其值的

1
2
3
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5
  1. 引用类型的比较是引用的比较

每次我们对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。

1
2
3
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5

虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象,所以他们是不相等的。

传值与传址

进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中。

1
2
3
4
5
6
var a = 10;
var b = a;

a ++ ;
console.log(a); // 11
console.log(b); // 10

引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。

1
2
3
4
5
6
7
8
9
10
11
12
var a = {}; // a保存了一个空对象的实例
var b = a; // a和b都指向了这个空对象

a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'

b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22

console.log(a == b);// true

赋值(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
    27
    let 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
2
3
4
5
6
7
8
9
10
11
12
13
var obj1 = {
foo: 'bar',
o: {
a: 'a',
b: 'b'
}
};

var clonedObj = { ...obj1 };
clonedObj.o.foo = 'foo';
clonedObj.o.a = 'aa';
obj1.o.foo // bar
obj1.o.a // aa

展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。

  • Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

1
2
3
4
5
6
7
8
9
10
11
12
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
// ["1", [4, 3]]

说明 slice() 方法是浅拷贝,相应的还有 concat 等,在工作中面对复杂数组结构要额外注意。

深拷贝(Deep Copy)

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

JSON.parse(JSON.stringify(object))

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
27
28
29
30
31
32
33
let a = {
name: "xiaoming",
parents: {
father: "daming",
mother: "dahua"
},
list: [0, "1", [2, 3]],
a: undefined,
fn: function(){console.log('123')}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "xiaoming",
// parents: {father: "daming", mother: "dahua"},
// list = [0, "1", [2, 3]]
// }

a.name = "zhou";
a.parents.father = "dazhou";
console.log(a);
// {
// name: "zhou",
// parents: {father: "dazhou", mother: "dahua"},
// list = [0, "1", [2, 3]]
// }

console.log(b);
// {
// name: "xiaoming",
// parents: {father: "daming", mother: "dahua"},
// list = [0, "1", [2, 3]]
// }

但是该方法有以下几个问题。

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 能解决循环引用的对象
  • 不能正确处理new Date()
  • 不能处理正则

undefinedsymbol 和函数这三种情况,会直接忽略。

除了上面介绍的深拷贝方法,常用的还有jQuery.extend()lodash.cloneDeep().

总结

和原数据是否指向同一对象 第一层数据为基本数据类型 第一层数据为基本数据类型
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

赋值和浅拷贝的区别

对于基本数据类型,赋值和浅拷贝并无区别。

但是对于引用类型,两者有所区别。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};

var obj2 = obj1;

var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}

obj2.name = "lisi";
obj3.age = "20";

obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];

console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};

console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};

console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};

obj2 是赋值的对象。obj3 是浅拷贝的对象。

被赋值的变量(obj2)和源数据指向同一个对象。即使修改的是引用类型中的属性是基本数据类型,源数据也会被改变。因为他们指向的是同一对象。

而浅拷贝只复制一层对象的属性,并不包括对象里面的引用类型的数据。

参考

【进阶4-1期】详细解析赋值、浅拷贝和深拷贝的区别
深拷贝与浅拷贝的实现(一)
js 深拷贝 vs 浅拷贝
展开语法 - JavaScript | MDN_