浅拷贝

首先浅拷贝和深拷贝只针对引用类型

浅拷贝:拷贝的是地址

常见方法:

拷贝对象

1.Object.assgin()

object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并。我们可以使用它来实现浅拷贝。

该方法的参数 target 指的是目标对象,sources指的是源对象。使用形式如下:

1
Object.assign(target, ...sources)

注:

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性;
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回;
  • 因为nullundefined 不能转化为对象,所以第一个参数不能为nullundefined,否则会报错;
  • 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。

实际上,Object.assign 会循环遍历原对象的可枚举属性,通过复制的方式将其赋值给目标对象的相应属性。

2. 扩展运算符

使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。使用形式如下:

1
let cloneObj = { ...obj };

拷贝数组

(1)Array.prototype.slice()

slice()方法是JavaScript数组方法,该方法可以从已有数组中返回选定的元素,不会改变原始数组。使用方式如下:

1
array.slice(start, end)

slice 方法不会修改原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说,slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

如果向两个数组任一中添加了新元素,则另一个不会受到影响。

(2)Array.prototype.concat()

concat() 方法用于合并两个或多个数组,此方法不会更改原始数组,而是返回一个新数组。使用方式如下:

1
arrayObject.concat(arrayX,arrayX,......,arrayX)

concat方法创建一个新的数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(参数是数组)或参数本身(参数不是数组)。它不会递归到嵌套数组参数中。

concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。 原始数组的元素将复制到新数组中,如下所示:

  • 对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。
  • 数据类型如字符串,数字和布尔值:concat将字符串和数字的值复制到新数组中。

手动实现浅拷贝

根据以上对浅拷贝的理解,实现浅拷贝的思路:

  • 对基础类型做最基本的拷贝;
  • 对引用类型开辟新的存储,并且拷贝一层对象属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}

这里用到了 hasOwnProperty() 方法,该方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。所有继承了 Object 的对象都会继承到 hasOwnProperty() 方法。这个方法可以用来检测一个对象是否是自身属性。

可以看到,所有的浅拷贝都只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝就无能为力了。深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。

深拷贝

首先浅拷贝和深拷贝只针对引用类型

深拷贝:拷贝的是对象,不是地址

常见方法:

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.stringify()实现

1.递归实现

函数递归:

如果一个函数在内部可以调用其本身,那么这个函数就是递归函数

  • 简单理解: 函数内部自己调用自己, 这个函数就是递归函数
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
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
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小宝'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
debugger
for (let k in oldObj) {
// 处理数组的问题 一定先写数组 在写 对象 不能颠倒
if (oldObj[k] instanceof Array) {
newObj[k] = []
// newObj[k] 接收 [] hobby
// oldObj[k] ['乒乓球', '足球']
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
}
else {
// k 属性名 uname age oldObj[k] 属性值 18
// newObj[k] === o.uname 给新对象添加属性
newObj[k] = oldObj[k]
}
}
}
deepCopy(o, obj) // 函数调用 两个参数 o 新对象 obj 旧对象
console.log(o)
o.age = 20
o.hobby[0] = '篮球'
o.family.baby = '老宝'
console.log(obj)
console.log([1, 23] instanceof Object)
// 复习
// const obj = {
// uname: 'pink',
// age: 18,
// hobby: ['乒乓球', '足球']
// }
// function deepCopy({ }, oldObj) {
// // k 属性名 oldObj[k] 属性值
// for (let k in oldObj) {
// // 处理数组的问题 k 变量
// newObj[k] = oldObj[k]
// // o.uname = 'pink'
// // newObj.k = 'pink'
// }
// }
</script>
</body>

这样虽然实现了深拷贝,但也存在一些问题:

  • 不能复制不可枚举属性以及 Symbol 类型;
  • 只能对普通引用类型的值做递归复制,对于 Date、RegExp、Function 等引用类型不能正确拷贝;
  • 可能存在循环引用问题。

优化递归实现

上面只是实现了一个基础版的深拷贝,对于上面存在的几个问题,可以尝试去解决一下:

  1. 使用 Reflect.ownKeys() 方法来解决不能复制不可枚举属性以及 Symbol 类型的问题。 Reflect.ownKeys() 方法会返回一个由目标对象自身的属性键组成的数组。它的返回值等同于: Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));
  2. 当参数值为 Date、RegExp 类型时,直接生成一个新的实例并返回;
  3. 利用 Object.getOwnPropertyDescriptors() 方以获得对象的所有属性以及对应的特性。简单来说,这个方法返回给定对象的所有属性的信息,包括有关getter和setter的信息。它允许创建对象的副本并在复制所有属性(包括getter和setter)时克隆它。
  4. 使用 Object.create() 方法创建一个新对象,并继承传入原对象的原型链。Object.create()方法会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  5. 使用 WeakMap 类型作为 Hash 表,WeakMap 是弱引用类型,可以防止内存泄漏,所以可以用来检测循环引用,如果存在循环,则引用直接返回 WeakMap 存储的值。WeakMap的特性就是,保存在其中的对象不会影响垃圾回收,如果WeakMap保存的节点,在其他地方都没有被引用了,那么即使它还在WeakMap中也会被垃圾回收回收掉了。在深拷贝的过程当中,里面所有的引用对象都是被引用的,为了解决循环引用的问题,在深拷贝的过程中,希望有个数据结构能够记录每个引用对象有没有被使用过,但是深拷贝结束之后这个数据能自动被垃圾回收,避免内存泄漏。
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
function deepClone (obj, hash = new WeakMap()) {
// 日期对象直接返回一个新的日期对象
if (obj instanceof Date){
return new Date(obj);
}
//正则对象直接返回一个新的正则对象
if (obj instanceof RegExp){
return new RegExp(obj);
}
//如果循环引用,就用 weakMap 来解决
if (hash.has(obj)){
return hash.get(obj);
}
// 获取对象所有自身属性的描述
let allDesc = Object.getOwnPropertyDescriptors(obj);
// 遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
if(typeof obj[key] === 'object' && obj[key] !== null){
cloneObj[key] = deepClone(obj[key], hash);
} else {
cloneObj[key] = obj[key];
}
}
return cloneObj
}

测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let obj = {
num: 1,
str: 'str',
boolean: true,
und: undefined,
nul: null,
obj: { name: '对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('函数') },
date: new Date(1),
reg: new RegExp('/正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性'
});
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 将loop设置成循环引用的属性
let cloneObj = deepClone(obj)

console.log('obj', obj)
console.log('cloneObj', cloneObj)

这样基本就实现了多数数据类型的深拷贝,不过也还存在一些缺陷,比如Map和Set结构在这个方法中无法进行拷贝。

2.lodash库cloneDeep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<!-- 先引用 -->
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小宝'
}
}
const o = _.cloneDeep(obj)
console.log(o)
o.family.baby = '老宝'
console.log(obj)
</script>
</body>

lodash中深拷贝的源代码:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* value:需要拷贝的对象
* bitmask:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性
* customizer:定制的 clone 函数
* key:传入 value 值的 key
* object:传入 value 值的父对象
* stack:Stack 栈,用来处理循环引用
*/

function baseClone(value, bitmask, customizer, key, object, stack) {
let result
// 标志位
const isDeep = bitmask & CLONE_DEEP_FLAG // 深拷贝,true
const isFlat = bitmask & CLONE_FLAT_FLAG // 拷贝原型链,false
const isFull = bitmask & CLONE_SYMBOLS_FLAG // 拷贝 Symbol,true
// 自定义 clone 函数
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value)
}
if (result !== undefined) {
return result
}
// 非对象
if (!isObject(value)) {
return value
}
const isArr = Array.isArray(value)
const tag = getTag(value)
if (isArr) {
// 数组
result = initCloneArray(value)
if (!isDeep) {
return copyArray(value, result)
}
} else {
// 对象
const isFunc = typeof value == 'function'
if (isBuffer(value)) {
return cloneBuffer(value, isDeep)
}
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
result = (isFlat || isFunc) ? {} : initCloneObject(value)
if (!isDeep) {
return isFlat
? copySymbolsIn(value, copyObject(value, keysIn(value), result))
: copySymbols(value, Object.assign(result, value))
}
} else {
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
result = initCloneByTag(value, tag, isDeep)
}
}
// 循环引用
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
// Map
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
// Set
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
// TypedArray
if (isTypedArray(value)) {
return result
}
// Symbol & 原型链
const keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys)

const props = isArr ? undefined : keysFunc(value)
// 遍历赋值
arrayEach(props || value, (subValue, key) => {
if (props) {
key = subValue
subValue = value[key]
}
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})

// 返回结果
return result
}

3.JSON序列化

JSON.parse(JSON.stringify(obj))是比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyJavaScript对象序列化成为JSON字符串),并将对象里面的内容转换成字符串,再使用JSON.parse来反序列化,将字符串生成一个新的JavaScript对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小宝'
}
}
// 把对象转换为 JSON 字符串
// console.log(JSON.stringify(obj))
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
o.family.baby = '123'
console.log(obj)
</script>
</body>

这个方法虽然简单粗暴,但也存在一些问题,在使用该方法时需要注意:

  • 拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 Date 引用类型会变成字符串;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  • 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。