深拷贝系列 ———— 自己通过递归实现一个深拷贝

深拷贝系列 ———— 什么是深拷贝、浅拷贝、Object.assign
深拷贝系列 ———— 自己实现一个 JSON.stringify 和 JSON.parse
深拷贝系列 ———— 自己通过递归实现一个深拷贝
深拷贝系列 ———— 分析 lodash 中的 deepcopy

简介

在上篇文章中我们深入了解了JSON.parse/JSON.stringify,并且自己实现了一个JSON.parse/JSON.stringify,在这篇文章中我们要自己实现一个深拷贝,并且解决JSON.parse/JSON.stringify中出现的问题。

递归实现

实现目标

  • 我们通过递归实现一个简单的深拷贝
  • 判断ObjectArray

实现步骤

  • 提前声明一个判断类型函数getType
  • 声明函数cloneDeep,首先判断原对象是否为object类型,如果不是直接返回原值
  • 声明一个新的目标对象newTarget,它的类型根据入参决定(Array、Object)
  • 通过for...in循环原对象,并且通过hasOwnProperty判断属性是否为本身属性
  • 如果是本身属性递归调用cloneDeep
  • 最后返回新对象newTarget
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
// 获取当前类型
function getType(attr) {
let type = Object.prototype.toString.call(attr);
let newType = type.substr(8, type.length - 9);
return newType;
}
// 声明一个函数
function cloneDeep (target) {
// 判断是否传入类型为Object
if (typeof target !== 'object') {
return target;
}
// 声明新对象
let newTarget = getType(target) === 'Array' ? [] : {};
// 循环对象 递归复制给新对象
for (let key in target) {
// 判断属性是否在对象本身上
if (target.hasOwnProperty(key)) {
// 递归调用
newTarget[key] = cloneDeep(target[key]);
}
}
// 返回新对象
return newTarget;
}

// 测试代码
const target = {
val1: 1,
val2: undefined,
val4: 'target',
val5: {
name: 'target',
age: function () {},
sym: Symbol('setter')
}
};
const targetArray = [1, 2, 3, {name: '123', age: 789}];
console.log(cloneDeep(target));
console.log(cloneDeep(targetArray));

测试效果图如下:

深拷贝/浅拷贝

在上面的代码中,已经解决了JSON.stringify/JSON.parse中的忽略undefined/function的问题,下面会逐渐解决问题,并且优化到类似与lodash库中的问题。

循环引用

实现目标

  • 解决循环引用对象问题
  • 解决引用丢失问题

实现步骤

  • 首先了解MapWeakMap是什么
  • 通过MapWeakMapArray储存属性对象
  • 如果再次使用,直接从MapWeakMapArray中取出(这样既解决了循环引用,又解决了引用丢失)

测试循环引用代码:

1
2
3
4
5
6
7
8
9
10
11
12
const target = {
val1: 1,
val2: undefined,
val4: "target",
val5: {
name: "target",
age: function() {},
sym: Symbol("setter")
}
};
target.target = target;
console.log(cloneDeep(target));

执行效果如下图所示:
深拷贝/浅拷贝

Map/weakMap

MapWeakMap都是ES6中的新出的数据类型。如果有兴趣可以去WeakMap mdnMap mdn,或者Set 和 Map 数据结构去了解它们的api使用场景等。

Map

Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

语法

new Map([iterable])

  • iterable: Iterable 可以是一个数组或者其他 iterable 对象,其元素为键值对(两个元素的数组,例如: [[ 1, ‘one’ ],[ 2, ‘two’ ]])。 每个键值对都会添加到新的Mapnull 会被当做 undefined

常用方法

  • Map.prototype.get(key): 返回键对应的值,如果不存在,则返回undefined
  • Map.prototype.set(key, value): 设置Map对象中键的值。返回该Map对象
  • Map.prototype.has(key): 返回一个布尔值,表示Map实例是否包含键对应的值。
  • Map.prototype.delete(key): 如果 Map对象中存在该元素,则移除它并返回 true;否则如果该元素不存在则返回 false

这里只介绍了常用的操作方法Map还有循环方法其他方法等等。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var oneMap = new Map();
var keyObj = {
name: 'keyObj'
};
oneMap.set('name', 'test'); // {"name" => "test"}
oneMap.set(keyObj, 'keyObj'); // {"name" => "test", keyObj => "keyObj"} 这里的keyObj是上面声明的对象

oneMap.get('name'); // "test"
oneMap.get(keyObj); // "keyObj"

oneMap.has('name'); // true
oneMap.has(keyObj); // true
oneMap.has('age'); // false

oneMap.delete('name'); // true

WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

它的语法、参数与Map是一至的,只有两点区别:

  • WeakMap只接受对象作为名(null除外),不接受其他类型的值作为键名
  • WeakMap 弱引用只是键名,而不是键值键值依然是正常引用

同时WeakMap只有上面get/set/has/delete四种方法,其它的方法它都是没有的。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}

这里就不测试它的操作方法了,它的操作方法与Map一至。

通过Map解决

在这里我们通过Map来解决循环引用,修改代码如下:

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
// 声明一个函数
function cloneDeep (target, map = new Map()) {
// 判断是否传入类型为Object
if (typeof target !== 'object') {
return target;
}
// 声明新对象
let newTarget = getType(target) === 'Array' ? [] : {};

// <!------新增代码开始------!>
// 查询map中是否有存在原对象(target),如果存在直接返回
if (map.get(target)) {
return target;
}
// 如果map中不存在原对象(target),则储存进map中
map.set(target, newTarget);
// <!------新增代码结束------!>

// 循环对象 递归复制给新对象
for (let key in target) {
// 判断属性是否在对象本身上
if (target.hasOwnProperty(key)) {
// 递归调用
newTarget[key] = cloneDeep(target[key]);
}
}
// 返回新对象
return newTarget;
}

执行测试代码如下:

1
2