Leecason

vuePress-theme-reco Leecason    2018 - 2020
Leecason Leecason

Choose mode

  • dark
  • auto
  • light
主页
分类
  • CSS
  • FrontEnd
  • GraphQL
  • JavaScript
  • TypeScript
  • Vue
  • Webpack
  • 其它
  • 数据结构与算法
  • 浏览器相关
标签
时间线
GitHub
author-avatar

Leecason

80

Article

61

Tag

主页
分类
  • CSS
  • FrontEnd
  • GraphQL
  • JavaScript
  • TypeScript
  • Vue
  • Webpack
  • 其它
  • 数据结构与算法
  • 浏览器相关
标签
时间线
GitHub

手写「浅拷贝」与「深拷贝」

vuePress-theme-reco Leecason    2018 - 2020

手写「浅拷贝」与「深拷贝」

Leecason 2019-09-03 浅拷贝深拷贝手写源码系列

# 定义

  • 浅拷贝:创建一个新对象,对原始对象精确拷贝。如果是基础类型,拷贝基础类型的值,如果是引用类型,拷贝内存地址

  • 深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象

# 浅拷贝

function clone (target) {
  let cloneTarget = {};
  for (const key in target) {
    cloneTarget[key] = target[key];
  }

  return cloneTarget;
}
1
2
3
4
5
6
7
8

# 深拷贝

# 乞丐版

JSON.parse(JSON.stringify());
1

缺陷:

  • 拷贝其他引用类型
  • 拷贝函数
  • 循环引用

# 基础版

  • 在浅拷贝的基础上,如果是引用类型,则创建一个新对象,遍历克隆对象的属性,深拷贝后依次添加到新对象上
  • 使用递归深拷贝对象




 







function clone (target) {
  if (typeof target === 'object') {
    let cloneTarget = {};
    for (const key in target) {
      cloneTarget[key] = clone(target[key]); // 递归
    }
    return cloneTarget;
  } else {
    return target;
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 考虑数组



 









function clone (target) {
  if (typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {}; // 考虑数组
    for (const key in target) {
      cloneTarget[key] = clone(target[key]); // 递归
    }
    return cloneTarget;
  } else {
    return target;
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 循环引用

递归死循环会导致内存溢出

使用 Map 来存储已经拷贝过的值






 
 
 
 
 









function clone (target, map = new Map()) {
  if (typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {}; // 考虑数组

    // 检查是否已经拷贝过,有的话直接返回
    if (map.has(target)) {
      return map.get(target);
    }

    map.set(target, cloneTarget);
    for (const key in target) {
      cloneTarget[key] = clone(target[key], map); // 递归
    }
    return cloneTarget;
  } else {
    return target;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# WeakMap

WeakMap 的键为弱引用。

弱引用,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则是不可访问的,因此可以在任何时刻被回收。

let obj = { name: 'obj' };
const target = new Map();
target.set(obj, 'target');
obj = null;
// 此时虽然手动进行释放,但是
// target 依然对 obj 存在强引用关系,所以这部分内存依然无法释放
1
2
3
4
5
6
let obj = { name: 'obj' };
const target = new WeakMap();
target.set(obj, 'target');
obj = null;
// WeakMap 的话,target 和 obj 存在的是弱引用关系
// 当下一次垃圾回收机制执行时,这块内存就会被释放
1
2
3
4
5
6

之前代码改写为:

 



function clone (target, map = new WeakMap()) {
  ...
}
1
2
3

# 性能优化

for...in 在遍历时效率低,while > for > for...in

所以使用 while 来替换 for...in

 
 
 
 
 
 
 
 
 
 












 
 
 
 
 
 
 
 







// 使用 `while` 实现一个 `forEach`,当遍历数组时,使用 forEach
function forEach (array, iteratee) {
  let index = -1;
  const length = array.length;
  while (++index < length) {
    iteratee(array[index], index)
  }

  return array;
}

function clone (target, map = new WeakMap()) {
  if (typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {}; // 考虑数组

    // 检查是否已经拷贝过,有的话直接返回
    if (map.has(target)) {
      return map.get(target);
    }

    map.set(target, cloneTarget);

    const keys = isArray ? undefined : Object.keys(target);

    forEach(keys || target, (value, key) => {
      if (keys) {
        key = value;
      }
      cloneTarget[key] = clone(target[key], map); // 递归
    });

    return cloneTarget;
  } else {
    return target;
  }
}
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

# 其它数据类型

# 可继续遍历引用类型

我们只考虑了 object 和 array,还有其它的引用类型需要考虑

// 判断是否为引用类型
function isObject (target) {
  return target !== null && (typeof target === 'object');
}

// 获取数据类型
function getType(target) {
  return Object.prototype.toString.call(target).slice(8, -1);
}

// 保留对象原型上的数据,若使用 cloneTarget = [] || {} 则会丢失原型
function getInit (target) {
  const Ctor = target.constructor;
  return new Ctor();
}

// 可继续遍历类型
const deepTag = ['Object', 'Array', 'Map', 'Set', 'Arguments'];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

改写之前代码:


 
 
 
 
 
 
 
 
 
 
 
 
 








 
 
 
 
 
 
 
 
 
 
 
 
 














function clone (target, map = new WeakMap()) {
   // 克隆原始类型
  if (!isObject(target)) {
    return target;
  }

  // 初始化
  const type = getType(target);
  let cloneTarget;

  // 判断是否可以继续遍历
  if (deepTag.includes(type)) {
    cloneTarget = getInit(target);
  }

  // 检查是否已经拷贝过,有的话直接返回
  if (map.has(target)) {
    return map.get(target);
  }

  map.set(target, cloneTarget);

  // 克隆 Set
  if (type === 'Set') {
    target.forEach(value => {
      cloneTarget.add(clone(value));
    });
  }

  // 克隆 Map
  if (type === 'Map') {
    target.forEach((value, key) => {
      cloneTarget.set(key, clone(value));
    });
  }

  // 克隆数组和对象
  const keys = type === 'Array' ? undefined : Object.keys(target);

  forEach(keys || target, (value, key) => {
    if (keys) {
      key = value;
    }
    cloneTarget[key] = clone(target[key], map);
  });

  return cloneTarget;
}
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

# 不可继续遍历引用类型

function cloneReg (target) {
  const reFlags = /\w*$/;
  const result = new target.constructor(target.source, reFlags.exec(target));
  result.lastIndex = target.lastIndex;
  return result;
}

function cloneSymbol (target) {
  return Object(Symbol.prototype.valueOf.call(target));
}

function cloneOtherType (target, type) {
  const Ctor = target.constructor;
  switch (type) {
    case 'Boolean':
    case 'Number':
    case 'String':
    case 'Error':
    case 'Date':
      return new Ctor(target);
    case 'RegExp':
      return cloneReg(target);
    case 'Symbol':
      return cloneSymbol(target);
    default:
      return null;
  }
}
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 clone (target, map = new WeakMap()) {
  ...

  // 判断是否可以继续遍历
  if (deepTag.includes(type)) {
    cloneTarget = getInit(target);
  } else {
    return cloneOtherType(target, type);
  }
  ...
}
1
2
3
4
5
6
7
8
9
10
11

# 克隆函数

可直接返回,也可以构造一个新函数














 
 





function cloneOtherType (target, type) {
  const Ctor = target.constructor;
  switch (type) {
    case 'Boolean':
    case 'Number':
    case 'String':
    case 'Error':
    case 'Date':
      return new Ctor(target);
    case 'RegExp':
      return cloneReg(target);
    case 'Symbol':
      return cloneSymbol(target);
    case 'Function':
      return target;
    default:
      return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 完整代码

// 使用 `while` 实现一个 `forEach`,当遍历数组时,使用 forEach
function forEach (array, iteratee) {
  let index = -1;
  const length = array.length;
  while (++index < length) {
    iteratee(array[index], index)
  }

  return array;
}

// 判断是否为引用类型
function isObject (target) {
  return target !== null && (typeof target === 'object');
}

// 获取数据类型
function getType(target) {
  return Object.prototype.toString.call(target).slice(8, -1);
}

// 保留对象原型上的数据,若使用 cloneTarget = [] || {} 则会丢失原型
function getInit (target) {
  const Ctor = target.constructor;
  return new Ctor();
}

// 可继续遍历类型
const deepTag = ['Object', 'Array', 'Map', 'Set', 'Arguments'];

function cloneReg (target) {
  const reFlags = /\w*$/;
  const result = new target.constructor(target.source, reFlags.exec(target));
  result.lastIndex = target.lastIndex;
  return result;
}

function cloneSymbol (target) {
  return Object(Symbol.prototype.valueOf.call(target));
}

// 克隆不可继续遍历引用类型
function cloneOtherType (target, type) {
  const Ctor = target.constructor;
  switch (type) {
    case 'Boolean':
    case 'Number':
    case 'String':
    case 'Error':
    case 'Date':
      return new Ctor(target);
    case 'RegExp':
      return cloneReg(target);
    case 'Symbol':
      return cloneSymbol(target);
    case 'Function':
      return target;
    default:
      return null;
  }
}

function clone (target, map = new WeakMap()) {
   // 克隆原始类型
  if (!isObject(target)) {
    return target;
  }

  // 初始化
  const type = getType(target);
  let cloneTarget;

  // 判断是否可以继续遍历
  if (deepTag.includes(type)) {
    cloneTarget = getInit(target);
  } else {
    return cloneOtherType(target, type);
  }

  // 检查是否已经拷贝过,有的话直接返回
  if (map.has(target)) {
    return map.get(target);
  }

  map.set(target, cloneTarget);

  // 克隆 Set
  if (type === 'Set') {
    target.forEach(value => {
      cloneTarget.add(clone(value));
    });
  }

  // 克隆 Map
  if (type === 'Map') {
    target.forEach((value, key) => {
      cloneTarget.set(key, clone(value));
    });
  }

  // 克隆数组和对象
  const keys = type === 'Array' ? undefined : Object.keys(target);

  forEach(keys || target, (value, key) => {
    if (keys) {
      key = value;
    }
    cloneTarget[key] = clone(target[key], map);
  });

  return cloneTarget;
}
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112

代码地址

# 参考

  • 如何写出一个惊艳面试官的深拷贝