浅拷贝和深拷贝?

重要: 什么是拷贝?

首先来直观的感受一下什么是拷贝。

1
2
3
4
5
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]

这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。

现在进行浅拷贝:

1
2
3
4
5
let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr);//[1, 2, 3]

当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!

这就是浅拷贝!

但是这又会带来一个潜在的问题:

1
2
3
4
5
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;

console.log(arr);//[ 1, 2, { val: 1000 } ]

咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。

这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。

接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?

浅拷贝

1. 手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 浅拷贝
const shallowClone = source =>{
if(typeof source === 'object' && source !== null){
const output = Array.isArray(source) ? [] : {} ;
for (let key in source){
if(source.hasOwnProperty(key)){
output[key] = source[key];
}
}
return output;
}else{
return source;
}
}

2. Object.assign

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

1
Object.assign(target, ...sources)
1
2
3
let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }

3. concat浅拷贝数组

concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

1
2
3
4
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

4. slice浅拷贝

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

5. …展开运算符

1
2
let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果

6. Array.from

1
2
3
4
5
6
7
let arr = [1, 2, { val: 4 }, () => { }];
arr.bar=arr;
let newArr3 = Array.from(arr);//浅拷贝
newArr3[0] = 2000;
newArr3[2].val = 2000;
console.log(arr);// [1, 2, {val: 2000}, ƒ, bar: Array(4)]
console.log(newArr3);// [2000, 2, {val: 2000}, ƒ]

深拷贝

第一种:JSON.parse(JSON.stringify(object))

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

  1. 无法解决循环引用的问题。举个例子:
1
2
const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  1. 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map等。

  2. 无法拷贝函数(划重点)。

  3. 会忽略 undefined

  4. 会忽略 symbol

就目前而言,第三方最完善的深拷贝方法是 Lodash 库的 _.cloneDeep() 方法了。在实际项目中,如需处理 JSON.stringify() 无法解决的 Case,我会推荐使用它

面试够用版

主要运用到递归的思路去实现一个深拷贝方法。

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
// 简易版本
const deepClone = source =>{
// 判断是否为数组
const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]';
// 判断是否为引用类型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function');
//拷贝(递归)
const copy = input =>{
// 当输入为函数或基本数据类型时,直接返回
if (typeof input === 'function' || !isObject(input)) return input;

const output = isArray(input) ? [] : {} ;
for(let key in input){
// 如果key是对象的自有属性
if(input.hasOwnProperty(key)){
// 递归调用深拷贝方法
output[key] = copy(input[key]);
}
}
return output;
}
return copy(source);
}
// 测试
let arr = [1, 2, {val: 4}];
let newArr1 = deepClone(arr);//深拷贝
newArr1[2].val = 1000;
let newArr2 = newArr1.slice();//浅拷贝
newArr2[2].val = 2000;


console.log(arr);//[ 1, 2, { val: 1000 } ]
console.log(newArr1);
console.log(newArr2)

需要进一步改进的问题

主要有循环引用、包装对象、函数、原型链、不可枚举属性、Map/WeakMap、Set/WeakSet、RegExp、Symbol、Date、ArrayBuffer、原生 DOM/BOM 对象等。

针对循环引用的问题

以下是一个循环引用(circular reference)的对象:

1
2
const foo = { name: 'Frankie' }
foo.bar = foo

使用WeakMap解决

首先,Map 的键属于强引用,而 WeakMap 的键则属于弱引用。且 WeakMap 的键必须是对象,WeakMap 的值则是任意的。

由于它们的键与值的引用关系,决定了 Map 不能确保其引用的对象不会被垃圾回收器回收的引用。假设我们使用的 Map,那么 foo 对象和我们深拷贝内部的 const map = new Map() 创建的 map 对象一直都是强引用关系,那么在程序结束之前,foo 不会被回收,其占用的内存空间一直不会被释放。

相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。

基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 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
29
30
31
// 深拷贝简易版本
const deepClone = source => {
// 创建一个 WeakMap 对象,记录已拷贝过的对象
const weakmap = new WeakMap();
// 判断是否为数组
const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]';
// 判断是否为引用类型
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function');
//拷贝(递归)
const copy = input => {
// 当输入为函数或基本数据类型时,直接返回
if (typeof input === 'function' || !isObject(input)) return input;
// 针对已拷贝过的对象,直接返回
if (weakmap.has(input)) {
return weakmap.get(input)
}
const output = isArray(input) ? [] : {};
// 记录每次拷贝的对象(需要放在递归引用的前面)
weakmap.set(input, output);
for (let key in input) {
// console.log(key)
// 如果key是对象的自有属性
if (input.hasOwnProperty(key)) {
// 递归调用深拷贝方法
output[key] = copy(input[key]);
}
}
return output;
}
return copy(source);
}

这里提供另一个思路,也是可以的。

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
const deepCopy = source => {
// 其他一样,省略一万字...

// 创建一个数组,将每次拷贝的对象放进去
const copiedArr = []

// 拷贝(递归思路)
const copy = input => {
// 当输入为函数或基本数据类型时,直接返回
if (typeof input === 'function' || !isObject(input)) return input;
// 循环遍历,若有已拷贝过的对象,则直接放回,以解决循环引用的问题
for (let i = 0, len = copiedArr.length; i < len; i++) {
if (input === copiedArr[i].key) return copiedArr[i].value
}

const output = isArray(input) ? [] : {}

// 记录每一次的对象
copiedArr.push({ key: input, value: output })

// 后面的流程不变...
}

return copy(source)
}