0%

从赋值深入深浅拷贝原理

理解: 什么是赋值?
将一个数值或对象赋给某个变量的过程。

赋值分两种:

  • 基本数据类型:赋值后两个变量互不影响
  • 引用数据类型:赋,两个变量具有相同的引用,指向同一个对象,相互之间有影响。
1
2
3
4
5
6
7
8
9
10
let a = "lkt";
let b = a;
console.log(a);
// lkt

a = "boy";
console.log(a);
// boy
console.log(b);
// lkt

对引用类型进行赋址操作,两个变量指向同一个对象,改变变量 a 之后会影响变量 b,哪怕改变的只是对象 a 中的基本类型数据。

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: "lkt",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = a;
console.log(b);
// {
// name: "lkt",
// book: {title: "You Don't Know JS", price: "45"}
// }

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }

console.log(b);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }

通常在开发中并不希望改变变量 a 之后会影响到变量 b,这时就需要用到浅拷贝和深拷贝。

浅拷贝

当我们创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。
如果属性是引用类型,拷贝的就是内存地址。
所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

123
上图中,SourceObject 是原对象,其中包含基本类型属性 field1 和引用类型属性 refObj。浅拷贝之后基本类型数据 field2 和 filed1 是不同属性,互不影响。但引用类型 refObj 仍然是同一个,改变之后会对另一个对象产生影响。

实际上就是:浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

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
let a = {
name: "lkt",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = Object.assign({}, a);
console.log(b);
// {
// name: "lkt",
// book: {title: "You Don't Know JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }

console.log(b);
// {
// name: "lkt",
// book: {title: "You Don't Know JS", price: "55"}
// }

上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 book 时,对象 b 相应的位置也发生了变化。

展开语法 Spread(实际效果和 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: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = {...a};
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }

console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "55"}
// }
  • Array.prototype.slice()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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]]

改变 a[1] 之后 b[0] 的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝,相应的还有concat等,在工作中面对复杂数组结构要额外注意。

深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。

当对象和它所引用的对象一起拷贝时即发生深拷贝。
深拷贝相比于浅拷贝速度较慢并且花销大。(拷贝前后两个对象互不影响)

image

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: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }

console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }

完全改变变量 a 之后对 b 没有任何影响,这就是深拷贝的魔力。

我们看下对数组深拷贝效果如何。

1
2
3
4
5
6
7
8
9
10
11
12
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( 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", [2, 3]]

此方法带来的问题:

  1. 忽略undefined、symbol。
  2. 不能序列化函数
  3. 不能解决循环引用的对象
  4. 不能正确处理new Date()
  5. 不能处理正则

undefined、symbol 和函数这三种情况,会直接忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj = {
name: 'muyiy',
a: undefined,
b: Symbol('muyiy'),
c: function() {}
}
console.log(obj);
// {
// name: "muyiy",
// a: undefined,
// b: Symbol(muyiy),
// c: ƒ ()
// }

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}

循环引用情况下,会报错

1
2
3
4
5
6
7
8
9
10
11
12
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON

new Date 情况下,转换错误

1
2
3
4
5
6
7
8
new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

解决方法转成字符串或者时间戳就好。

1
2
3
4
5
6
7
8
let date = (new Date()).valueOf();
// 1545620645915

JSON.stringify(date);
// "1545620673267"

JSON.parse(JSON.stringify(date));
// 1545620658688

正则情况下

1
2
3
4
5
6
7
8
9
10
let obj = {
name: "muyiy",
a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}

深拷贝也可以直接用lodash库。