深拷贝与浅拷贝

  • 拷贝就是创建一个已有对象或数据结构的副本的过程,即拷贝是相对于引用类型而言的。

  • 对于 JS 来说,浅拷贝仅复制对象的第一层属性,对于其对象属性,复制的是其存储的地址值。因此,通过浅拷贝得到的副本,修改其内部的对象属性,也会影响原对象。

  • 深拷贝会复制对象的所有属性,包括第一层中的对象属性,即对象属性会在堆区开辟新的空间(新的地址值),其存储值与被拷贝的内容相同,因此,通过深拷贝得到的副本,修改其内部的对象属性,不会影响原对象。

浅拷贝的实现

1. 使用 Object.assign()

Object.assign() 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。

语法

Object.assign(target, …sources)

  • target:需要应用源对象属性的目标对象,修改后将作为返回值。

  • sources:一个或多个包含要应用的属性的源对象。

如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

1
2
3
let a = {name:'1'}
let copy = Object.assign({},a)
console.log(copy); // {name: '1'}

字符串和 Symbol 类型属性都会被复制。

Object.assign() 不会在源对象值为 null 或 undefined 时抛出错误。基本类型将被封装,只有字符串封装对象才拥有可枚举的自有属性。因此,除了字符串,其他基本类型都不会被处理。

1
2
3
4
5
6
7
8
9
10
11
12
 // {0: 'c', 1: 'd', 2: 'e', 3: '3', 4: '4', a: 1, b: 2}
console.log(Object.assign(
{},
{ a: 1 },
null,
undefined,
{ b: 2 },
"cde34",
true,
11,
Symbol('11'),
11n));

特点:

  • 合并具有相同属性的对象,属性会被后续参数中具有相同属性的其他对象覆盖。

  • 可以拷贝 Symbol 类型属性。

  • 原型链上的属性和不可枚举的属性不能被复制。

  • 基本类型会被封装为对象。

  • 异常会中断后续的复制。

2. 使用扩展运算符 ...

展开语法 (Spread syntax), 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。(译者注: 字面量一般指 [1, 2, 3] 或者 {name: “mdn”} 这种简洁的构造方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 数组拷贝
var arr = [1, 2, 3];
var arr2 = [...arr];
arr2.push(4);
console.log(arr2); // (4) [1, 2, 3, 4]
// 数组合并
var arr2 = [0, 1, 2];
var arr3 = [3, 4, 5];
var arr4 = [...arr2, ...arr3]; // [0, 1, 2, 3, 4, 5]

// 对象拷贝
var obj1 = { foo: "bar", x: 42 };
var obj2 = { foo: "baz", y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象:{ foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象:{ foo: "baz", x: 42, y: 13 }

特点:

  • 只能用于可迭代对象,扩展运算符只能拷贝对象自身的并且可枚举的属性。

  • Object.assign() 函数会触发 setters,而展开语法则不会。

  • 不能替换或者模拟 Object.assign() 函数

  • 在函数调用时使用展开语法,请注意不能超过 JavaScript 引擎限制的最大参数个数(调用具有太多参数的函数(即超过数万个参数)的后果是未指定的,并且在不同的引擎中会有所不同。)

3. 手写实现浅拷贝

在展开语法出来之前,我们想将已有数组或对象变成新数组或对象的一部分较为麻烦

  • 对数组,我们可以使用 Array.prototype.concat() 或 Array.prototype.slice() 方法,其实只要能够遍历该数组,然后创建一个空数组加入即可,没什么好说的。

  • 对对象,即创建一个空对象,然后想办法遍历原对象加入即可。

深拷贝的实现

1. JSON.stringify()

通过JSON.stringify()确实可以进行深拷贝,但局限于JSON的语法,JSON不支持undefined、Symbol、BigInt数据类型,所以通过JSON进行深拷贝时会出现问题:

  • 如果时undefined和Symbol类型,会直接忽略掉,不会出现在新对象中。

  • 如果时BinInt类型,会直接报错。

  • 对于循环引用会报错。

2. 函数库lodash

lodash的cloneDeep()方法,详细的可以看官网。
https://www.lodashjs.com/docs/lodash.cloneDeep/

3. 手写实现深拷贝

如果有更多定制化要求可以自己添加。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const obj = {
name: 'tom',
age: 18,
isHigh: false,
foo: undefined,
love: null,
hobbies: ['eat', 'sleep', 'code', { name: 111 }],
book: {
name: 'JS',
time: 20,
bar: undefined,
baz: null,
},
[Symbol(222)]: 222,
bar: Symbol(11),
bar1: new Set([1, 2, 3]),
fn: () => { }
}

obj.obj1 = obj

function isObject(value) {
const valueType = typeof value
return (value !== null) && (valueType === 'object' || valueType === 'function')
}

function deepClone(target, map = new WeakMap()) {

if (typeof target === 'symbol') {
return Symbol(target.description)
}

if (!isObject(target)) {
return target
}

if (typeof target === 'function') {
return target
}

if (target instanceof Set) {
const set = new Set()
for (let item of target) {
set.add(item)
}
return set
}

if (map.has(target)) {
return map.get(target)
}

const obj = Array.isArray(target) ? [] : {}
map.set(target, obj)


for (let key in target) {
obj[key] = deepClone(target[key], map)
}

for (let key of Object.getOwnPropertySymbols(target)) {
obj[Symbol(key.description)] = deepClone(target[key], map)
}
return obj
}

console.log(deepClone(obj))
/*
{name: 'tom', age: 18, isHigh: false, foo: undefined, love: null, …}age: 18bar: Symbol(11)bar1: Set(3) {1, 2, 3}book: {name: 'JS', time: 20, bar: undefined, baz: null}fn: () => { }foo: undefinedhobbies: (4) ['eat', 'sleep', 'code', {…}]isHigh: falselove: nullname: "tom"obj1: {name: 'tom', age: 18, isHigh: false, foo: undefined, love: null, …}Symbol(222): 222}
*/