浅拷贝 / 深拷贝:
- clone-deep
- JSON.stringify
- 尤雨溪的
circular-json-es6
- @jsmini/clone
- …
浅拷贝
浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
- Object.assign
- 展开运算符(…)
clone-deep
深拷贝需要考虑:
- JSON 克隆不支持函数、引用、undefined、Date、RegExp 等
- 递归克隆要考虑环
- 要考虑 Date、RegExp、Function 等特殊对象的克隆方式
- 要不要克隆 proto,如果要克隆,就非常浪费内存;如果不克隆,就不是深克隆
所以一般说的的深拷贝都是浅的,自己实现是很复杂可以考虑用个库 clone-deep
cloneDeep.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const cloneDeep = (obj) => {
if (!obj) {
return obj;
}
let res;
if (Array.isArray(obj)) {
res = [];
} else if (typeof obj === "object") {
res = {};
}
for (const item in obj) {
const val = obj[item];
if (typeof val === "object") {
res[item] = cloneDeep(val);
} else {
res[item] = val;
}
}
return res;
};
JSON.stringify
- 对象如果引用了自身的话是不能够直接 JSON.stringify 的,可以传入第二个参数 getCircularReplacer 函数解决
- 当值为函数、undefined、或 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
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value
function getCircularReplacer() {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}
window.postMessage(
JSON.stringify(
{
type: BlockErrorActions.ON_BLOCK_ERROR,
payload: {
error_entity: origin_error_entity_id,
block_id: error_block_id,
heart_error: e,
},
},
getCircularReplacer()
),
"*"
);
circular-json-es6
尤雨溪在知乎上有个回答,说了一下这个问题:
- 你任意对象的深度克隆,edge case 非常多,比如原生 DOM/BOM 对象怎么处理,RegExp 怎么处理,函数怎么处理,原型链怎么处理… 并不是一个简单的问题。
- 大部分时候 deep clone 的用例都是在数据结构的持久化上,换句话说应该是可以被序列化/反序列化的数据。数据类型只包含 JSON 支持的类型的话就好办了,加上循环引用支持就行了。
他写了一个方案 circular-json-es6
,代码十分的优雅。(https://github.com/yyx990803/circular-json-es6)
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
function encode(data, replacer, list, seen) {
var stored, key, value, i, l;
var seenIndex = seen.get(data);
if (seenIndex != null) {
return seenIndex;
}
var index = list.length;
if (isPlainObject(data)) {
stored = {};
seen.set(data, index);
list.push(stored);
var keys = Object.keys(data);
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i];
value = data[key];
if (replacer) {
value = replacer.call(data, key, value);
}
stored[key] = encode(value, replacer, list, seen);
}
} else if (Array.isArray(data)) {
stored = [];
seen.set(data, index);
list.push(stored);
for (i = 0, l = data.length; i < l; i++) {
value = data[i];
if (replacer) {
value = replacer.call(data, i, value);
}
stored[i] = encode(value, replacer, list, seen);
}
} else {
index = list.length;
list.push(data);
}
return index;
}
function decode(list, reviver) {
var i = list.length;
var j, k, data, key, value;
while (i--) {
var data = list[i];
if (isPlainObject(data)) {
var keys = Object.keys(data);
for (j = 0, k = keys.length; j < k; j++) {
key = keys[j];
value = list[data[key]];
if (reviver) value = reviver.call(data, key, value);
data[key] = value;
}
} else if (Array.isArray(data)) {
for (j = 0, k = data.length; j < k; j++) {
value = list[data[j]];
if (reviver) value = reviver.call(data, j, value);
data[j] = value;
}
}
}
}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
exports.stringify = function stringify(data, replacer, space) {
try {
return arguments.length === 1
? JSON.stringify(data)
: JSON.stringify(data, replacer, space);
} catch (e) {
return exports.stringifyStrict(data, replacer, space);
}
};
exports.parse = function parse(data, reviver) {
var hasCircular = /^\s/.test(data);
if (!hasCircular) {
return arguments.length === 1
? JSON.parse(data)
: JSON.parse(data, reviver);
} else {
var list = JSON.parse(data);
decode(list, reviver);
return list[0];
}
};
exports.stringifyStrict = function (data, replacer, space) {
var list = [];
encode(data, replacer, list, new Map());
return space
? " " + JSON.stringify(list, null, space)
: " " + JSON.stringify(list);
};
@jsmini/clone
原文地址:https://yanhaijing.com/javascript/2018/10/10/clone-deep/
可以生成指定深度和每层广度的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createData(deep, breadth) {
const data = {};
const temp = data;
for (let i = 0; i < deep; i++) {
temp = temp["data"] = {};
for (let j = 0; j < breadth; j++) {
temp[j] = j;
}
}
return data;
}
createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}
简易深拷贝:
1
2
3
function cloneJSON(source) {
return JSON.parse(JSON.stringify(source));
}
爆栈、循环引用
1
2
3
4
5
6
7
// 爆栈
cloneJSON(createData(10000)); // Maximum call stack size exceeded
// 循环引用
const a = {};
a.a = a;
cloneJSON(a); // Uncaught TypeError: Converting circular structure to JSON
cloneLoop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const a = {
a1: 1,
a2: {
b1: 1,
b2: {
c1: 1
}
}
}
a
/ \
a1 a2
| / \
1 b1 b2
| |
1 c1
|
1
用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了
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 cloneLoop(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;
}
cloneForce
假如一个对象 a,a 下面的两个键值都引用同一个对象 b,经过深拷贝后,a 的两个键值会丢失引用关系,从而变成两个不同的对象
1
2
3
4
5
6
7
const b = {};
const a = { a1: b, a2: b };
a.a1 === a.a2; // true
var c = clone(a);
c.a1 === c.a2; // false
引入一个数组 uniqueList
用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在 uniqueList
中了,如果在的话就不执行拷贝逻辑了。
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
function cloneForce(x) {
const uniqueList = []; // 用来去重
let 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] = {};
}
// 数据已经存在
let uniqueData = find(uniqueList, data);
if (uniqueData) {
parent[key] = uniqueData.target;
continue; // 中断本次循环
}
// 数据不存在,保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});
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;
}
function find(arr, item) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}
runTime
可以通过一个 runTime
函数检测:
1
2
3
4
5
6
7
8
9
10
function runTime(fn, time) {
const stime = Date.now();
let count = 0;
while (Date.now() - stime < time) {
fn();
count++;
}
return count;
}
一般性能优化是在遇到瓶颈的时候才去进行,有句话叫做:“先抗住,再优化”。如果只是少量数据的数据持久化的话,可能直接 JSON.stringify
就完事了。