克隆对象&&数组(二)
Jonnzer Lv4

4. 深拷贝

深拷贝 = 浅拷贝 + 遇到对象时递归拷贝
(1)最简单莫过于字符序列化,再parse反序列化
语法:JSON.parse(JSON.stringify)

后端传回的变量一般用它来拷贝足以应对。只是, 1、会忽略 undefined

2、会忽略 symbol

3、不能序列化函数

4、不能解决循环引用的对象

5、不能正确处理new Date()

6、不能处理正则

undefined、symbol 和函数这三种情况,会直接忽略。拷贝函数会undefined,拷贝正则会Object{},没拷完整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// undefined symbol 函数示例
let obj = {
name: 'muyiy',
a: undefined,
b: Symbol('muyiy'),
c: function() {}
}
console.log(obj);
// {
// name: "muyiy",
// a: undefined,
// b: Symbol(muyiy),
// c: ƒ ()
// }

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 循环引用
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
1
2
3
4
// new Date转化结果不正确 结果方案是转为时间戳
let date = (new Date()).valueOf();
JSON.stringify(date);
JSON.parse(JSON.stringify(date)); // 和上面的输出是同一个值
1
2
3
4
5
6
7
8
9
10
// 正则
let obj = {
name: "muyiy",
a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b); // {name: "muyiy", a: {}}

(2) 简易深拷贝

第一版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这个虽然能简单实现 却有以下问题
// 1、没有对传入参数进行校验,传入 null 时应该返回 null 而不是 {}
// 2、对于对象的判断逻辑不严谨,因为 typeof null === 'object'
// 3、没有考虑数组的兼容
function cloneDeep1(source) {
var target = {};
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object') {
target[key] = cloneDeep1(source[key]); // 注意这里 如果是对象的时候递归调用
} else {
target[key] = source[key];
}
}
}
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function isObject(obj) {// 非对象返回自身
return typeof obj === 'object' && obj != null;
}
function cloneDeep2(source) {
if (!isObject(source)) return source; // 非对象返回自身
var target = Array.isArray(source) ? [] : {}; // 初始值为空数组或空对象
for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep2(source[key]); // 注意这里
} else {
target[key] = source[key];
}
}
}
return target;
}

function deepclone(obj = {}) { // clear easy know version
if (typeof obj !== 'object' || obj == null) {
return obj
}
let target

if (obj instanceof Array) {
target = []
} else {
target = {}
}

for (let key in obj) {
if (obj.hasOwnPorperty(key)) {
target[key] = deepclone(obj[key])
}
}

return target
}


// 使用上面测试用例测试一下
var b = cloneDeep2(a);
console.log(b);
// {
// name: 'muyiy',
// book: { title: 'You Don\'t Know JS', price: '45' },
// a1: undefined,
// a2: null,
// a3: 123
// }

第三版 (支持对象循环引用)

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
function cloneDeep3(source, hash = new WeakMap()) {

if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表

var target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 新增代码,哈希表设值

for(var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep3(source[key], hash); // 新增代码,传入哈希表
} else {
target[key] = source[key];
}
}
}
return target;
}

const a = {
name: "muyiy",
a1: undefined,
a2: null,
a3: 123,
book: {title: "You Don't Know JS", price: "45"},
}
a.circleRef = a // circleRef是个很奇特的属性,它能在自身存自己,并永久循环,
var b = cloneDeep3(a)
console.log(b)

第四版 (保存引用关系)

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
// 数组版
function cloneDeep3(source, uniqueList) {
if (!isObject(source)) return source
if (!uniqueList) uniqueList = []

var target = Array.isArray(source) ? [] : {}

var uniqueData = find(uniqueList, source)

// 数据已经存在,返回保存的数据 如果return这一步,会跳到下方的return target
if(uniqueData) {
return uniqueData.target
}

// 数据不存在,保存源数据,以及对应的引用
uniqueList.push({ // 用数组存储键值对
source: source,
target: target
})

for (var key in source) {
if(Object.prototype.hasOwnProperty.call(source,key)) {
if (isObject(source[key])) {
target[key] = cloneDeep3(source[key], uniqueList)
} else {
target[key] = source[key]
}
}
}
return target // target很值得玩味,递归完了后的callStack让人惊奇
}

var obj1 = {};
var obj2 = {a: obj1, b: obj1};

console.log(obj2.a === obj2.b);
// true

var obj3 = cloneDeep3(obj2);
console.log(obj3.a === obj3.b)
// true

第五版 (拷贝Symbol类型)
思路就是先查找有没有 Symbol 属性,如果查找到则先遍历处理 Symbol 情况,然后再处理正常情况,

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
// 逻辑代码
function cloneDeep4(source, hash = new WeakMap()) {

if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source);

let target = Array.isArray(source) ? [] : {};
hash.set(source, target);

// ============= 新增代码
let symKeys = Object.getOwnPropertySymbols(source); // 查找
if (symKeys.length) { // 查找成功
symKeys.forEach(symKey => {
if (isObject(source[symKey])) {
target[symKey] = cloneDeep4(source[symKey], hash);
} else {
target[symKey] = source[symKey];
}
});
}
// =============

for(let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep4(source[key], hash);
} else {
target[key] = source[key];
}
}
}
return target;
}

// 测试 可以看到成功拷贝了Symbol类型
var sym1 = Symbol("a"); // 创建新的symbol类型
var sym2 = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol

a[sym1] = "localSymbol";
a[sym2] = "globalSymbol";

var b = cloneDeep4(a);
console.log(b);
// {
// name: "muyiy",
// a1: undefined,
// a2: null,
// a3: 123,
// book: {title: "You Don't Know JS", price: "45"},
// circleRef: {name: "muyiy", book: {…}, a1: undefined, a2: null, a3: 123, …},
// [Symbol(a)]: 'localSymbol',
// [Symbol(b)]: 'globalSymbol'
// }

第六版 破解递归爆栈 (待研究@todo)

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
function cloneDeep5(x) {
const root = {};

// 栈
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];

while(loopList.length) {
// 广度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;

// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}

for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}

return root;
}

小语法:

1 Object.keys(..) 返回一个数组,包含所有可枚举属性
2 Object.getOwnPropertyNames(..) 返回一个数组,包含所有属性,无论它们是否可枚举
3 Object.getOwnPropertyDescriptor() 会返回对象的属性的指定描述
4 in && hasOwnProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
var anotherObject = {
a: 1
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.b = 2;
("a" in myObject); // true
("b" in myObject); // true
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true

// (1) in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。
// (2) hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 原型链。

5 WeakMap
特点:弱引用,需以对象作为key,不打扰垃圾回收机制,可以通过set,get来取值

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
const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap();
const o1 = {},
o2 = function(){},
o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)

wm3.set(o1, 37);
wm3.get(o1); // 37

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

参考
MDN Object.assign
wengjq issue
yygmind issue
yygmind 深浅拷贝汇总
展开语法
MDN Object.getOwnPropertyDescription
MDN WeakMap
神奇的weakmap科普贴
颜海镜

  • 本文标题:克隆对象&&数组(二)
  • 本文作者:Jonnzer
  • 创建时间:2020-07-13 15:12:29
  • 本文链接:https://jonnzer.github.io/2020/07/13/克隆对象&&数组(二)/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论