JavaScript对象深度克隆的高效方法

JavaScript对象深度克隆的高效方法

技术背景

在JavaScript开发中,经常会遇到需要复制对象的场景。简单的赋值操作只会创建对象的引用,而不是对象的副本,这意味着修改副本会影响原始对象。为了创建一个独立于原始对象的副本,就需要进行对象克隆。浅克隆只复制对象的一层属性,而深度克隆会递归地复制对象的所有属性,包括嵌套对象,确保副本和原始对象完全独立。然而,JavaScript本身并没有提供直接的深度克隆方法,因此需要开发者自己实现或借助第三方库来完成。

实现步骤

1. 使用structuredClone函数(现代浏览器和Node.js >= 17)

1
2
3
4
// 直接使用structuredClone函数
const original = { name: 'John', age: 30 };
const clone = structuredClone(original);
console.log(clone);

注意事项:

  • 函数对象不能被structuredClone复制,尝试复制会抛出DataCloneError异常。
  • 克隆DOM节点同样会抛出DataCloneError异常。
  • 某些对象属性不会被保留,如RegExp对象的lastIndex属性、属性描述符、setter、getter等。

2. 使用JSON.parse(JSON.stringify())

1
2
3
const original = { name: 'John', age: 30 };
const clone = JSON.parse(JSON.stringify(original));
console.log(clone);

注意事项:

  • 如果对象中包含Date、函数、undefinedInfinityRegExpsMapsSetsBlobsFileListsImageDatas、稀疏数组、类型化数组等复杂类型,这些类型会丢失或被转换。例如,JSON.stringify(new Date())返回日期的字符串表示,JSON.parse()不会将其转换回Date对象。

3. 使用第三方库

Lodash的cloneDeep

1
2
3
4
const _ = require('lodash');
const original = { name: 'John', age: 30 };
const clone = _.cloneDeep(original);
console.log(clone);

Ramda的clone

1
2
3
4
const R = require('ramda');
const original = { name: 'John', age: 30 };
const clone = R.clone(original);
console.log(clone);

4. 自定义递归克隆函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function clone(obj) {
if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
return obj;

if (obj instanceof Date)
var temp = new obj.constructor();
else
var temp = obj.constructor();

for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
obj['isActiveClone'] = null;
temp[key] = clone(obj[key]);
delete obj['isActiveClone'];
}
}
return temp;
}

const original = { name: 'John', age: 30 };
const clone = clone(original);
console.log(clone);

核心代码

自定义递归克隆函数(处理循环引用)

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
function deepCopy(src, /* INTERNAL */ _visited, _copiesVisited) {
if(src === null || typeof(src) !== 'object'){
return src;
}

if(typeof src.clone == 'function'){
return src.clone(true);
}

if(src instanceof Date){
return new Date(src.getTime());
}
if(src instanceof RegExp){
return new RegExp(src);
}
if(src.nodeType && typeof src.cloneNode == 'function'){
return src.cloneNode(true);
}

if (_visited === undefined){
_visited = [];
_copiesVisited = [];
}

var i, len = _visited.length;
for (i = 0; i < len; i++) {
if (src === _visited[i]) {
return _copiesVisited[i];
}
}

if (Object.prototype.toString.call(src) == '[object Array]') {
var ret = src.slice();
_visited.push(src);
_copiesVisited.push(ret);

var i = ret.length;
while (i--) {
ret[i] = deepCopy(ret[i], _visited, _copiesVisited);
}
return ret;
}

var proto = (Object.getPrototypeOf ? Object.getPrototypeOf(src): src.__proto__);
if (!proto) {
proto = src.constructor.prototype;
}
var dest = Object.create(proto);
_visited.push(src);
_copiesVisited.push(dest);

for (var key in src) {
dest[key] = deepCopy(src[key], _visited, _copiesVisited);
}
return dest;
}

const original = { name: 'John', age: 30 };
const clone = deepCopy(original);
console.log(clone);

最佳实践

  • 选择合适的方法:根据对象的类型和复杂度选择合适的克隆方法。如果对象只包含简单的JSON数据,JSON.parse(JSON.stringify())是一个简单有效的方法;如果对象包含复杂类型或循环引用,建议使用第三方库或自定义递归克隆函数。
  • 性能优化:在性能敏感的场景中,避免使用JSON.parse(JSON.stringify()),因为它的性能相对较低。可以使用自定义递归克隆函数或手动内联克隆过程来提高性能。
  • 测试和验证:在使用克隆方法之前,进行充分的测试和验证,确保克隆的对象和原始对象是独立的,并且没有丢失重要的属性或方法。

常见问题

1. JSON.parse(JSON.stringify())丢失复杂类型

如前所述,JSON.parse(JSON.stringify())会丢失Date、函数、undefined等复杂类型。可以通过自定义解析函数来处理,例如:

1
2
3
4
5
6
7
8
9
10
11
12
function clone(obj) {
var regExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
return JSON.parse(JSON.stringify(obj), function(k, v) {
if (typeof v === 'string' && regExp.test(v))
return new Date(v)
return v;
})
}

const original = { date: new Date() };
const cloneObj = clone(original);
console.log(cloneObj);

2. 循环引用导致栈溢出

如果对象存在循环引用,递归克隆函数会导致栈溢出。可以通过记录已经克隆的对象来避免,如上述的deepCopy函数。

3. 性能问题

某些克隆方法(如JSON.parse(JSON.stringify()))性能较低,在处理大型对象或频繁克隆时会影响性能。可以通过优化算法或使用更高效的克隆方法来解决。


JavaScript对象深度克隆的高效方法
https://119291.xyz/posts/2025-04-17.efficient-ways-to-deep-clone-javascript-objects/
作者
ww
发布于
2025年4月17日
许可协议