在 JavaScript 中深度克隆对象的最有效方法是什么?
克隆 JavaScript 对象的最有效方法是什么?我见过有人使用
obj = eval(uneval(o));
,但
这是非标准的,并且仅受 Firefox 支持
。
我做过类似
obj = JSON.parse(JSON.stringify(o));
的事情,但对其效率存疑。
我还见过存在各种缺陷的递归复制函数。
我很惊讶没有规范的解决方案。
原生深度克隆
现在所有主流浏览器和 node >= 17 都支持
structuredClone(value)
函数。它具有
适用于旧系统的 polyfill
。
structuredClone(value)
如果需要,请先加载 polyfill:
import structuredClone from '@ungap/structured-clone';
- 函数对象不能被结构化克隆算法复制;尝试抛出 DataCloneError 异常。
- 克隆 DOM 节点同样会抛出 DataCloneError 异常。
-
某些对象属性未保留:
- RegExp 对象的 lastIndex 属性未保留。
- 属性描述符、setter、getter 和类似元数据的功能不会被复制。例如,如果对象使用属性描述符标记为只读,则它将在副本中被读取/写入,因为这是默认设置。
- 原型链不会被遍历或复制。
旧答案
快速克隆但数据丢失 - JSON.parse/stringify
如果您不使用
Date
、函数、
undefined
、
Infinity
、RegExp、Maps、Sets、Blob、FileLists、ImageDatas、稀疏数组、类型化数组或其他复杂类型,则深度克隆对象的一个非常简单的一行代码是:
JSON.parse(JSON.stringify(object))
const a = {
string: 'string',
number: 123,
bool: false,
nul: null,
date: new Date(), // stringified
undef: undefined, // lost
inf: Infinity, // forced to 'null'
re: /.*/, // lost
}
console.log(a);
console.log(typeof a.date); // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date); // result of .toISOString()
参见 Corban 的答案 用于基准测试。
使用库进行可靠克隆
由于克隆对象并不简单(复杂类型、循环引用、函数等),因此大多数主要库都提供了克隆对象的函数。 不要重新发明轮子 - 如果您已经在使用库,请检查它是否具有对象克隆功能。例如,
-
lodash -
cloneDeep
;可以通过 lodash.clonedeep 模块单独导入,如果您尚未使用提供深度克隆功能的库,这可能是您的最佳选择 -
Ramda -
clone
-
AngularJS -
angular.copy
-
jQuery -
jQuery.extend(true, { }, oldObject)
;.clone()
仅克隆 DOM 元素 -
just 库 -
just-clone
; 属于零依赖 npm 模块库的一部分,只做一件事。 适合各种场合的无负担实用程序。
查看此基准: http://jsben.ch/#/bWfk9
在我之前的测试中,速度是主要关注点,我发现
JSON.parse(JSON.stringify(obj))
是深度克隆对象最慢的方式(它比将
deep
标志设置为 true 的
jQuery.extend
慢 10-20%)。
当
deep
标志设置为
false
(浅克隆)时,jQuery.extend 非常快。这是一个很好的选择,因为它包含一些额外的类型验证逻辑,并且不会复制未定义的属性等,但这也会使您的速度稍微变慢。
如果您知道您尝试克隆的对象的结构或者可以避免深度嵌套数组,您可以编写一个简单的
for (var i in obj)
循环来克隆您的对象,同时检查 hasOwnProperty,它会比 jQuery 快得多。
最后,如果您试图在热循环中克隆已知对象结构,您可以通过简单地内联克隆过程并手动构造对象来获得更高的性能。
JavaScript 跟踪引擎在优化
for..in
循环方面很糟糕,检查 hasOwnProperty 也会减慢您的速度。当速度是绝对必要条件时,请手动克隆。
var clonedObject = {
knownProp: obj.knownProp,
..
}
请谨慎在
Date
对象上使用
JSON.parse(JSON.stringify(obj))
方法 -
JSON.stringify(new Date())
返回 ISO 格式的日期字符串表示形式,而
JSON.parse()
不会
转换回
Date
对象。
有关更多详细信息,请参阅此答案
。
此外,请注意,至少在 Chrome 65 中,本机克隆不是可行的方法。据 JSPerf 称,通过创建新函数执行本机克隆比使用 JSON.stringify 慢近 800 倍 ,而 JSON.stringify 的速度在整个过程中都非常快。
如果您使用的是 Javascript ES6,请尝试使用此本机方法进行克隆或浅拷贝。
Object.assign({}, obj);
结构化克隆
2022 年更新
:
structuredClone
全局函数
已在 Firefox 94、Node 17 和 Deno 1.14 中可用
HTML 标准包括 内部结构化克隆/序列化算法 ,可以创建对象的深度克隆。它仍然仅限于某些内置类型,但除了 JSON 支持的少数类型之外,它还支持日期、RegExp、地图、集合、Blob、文件列表、图像数据、稀疏数组、类型数组以及将来可能支持的更多类型。它还保留了克隆数据内的引用,使其能够支持会导致 JSON 错误的循环和递归结构。
Node.js 中的支持:
Node 17.0 提供了
structuredClone
全局函数
:
const clone = structuredClone(original);
以前的版本:Node.js 中的
v8
模块(从 Node 11 开始)
直接公开结构化序列化 API
,但此功能仍标记为“实验性”,可能会在未来版本中更改或删除。如果您使用的是兼容版本,则克隆对象非常简单:
const v8 = require('v8');
const structuredClone = obj => {
return v8.deserialize(v8.serialize(obj));
};
浏览器中的直接支持:在 Firefox 94 中可用
所有主流浏览器将很快提供
structuredClone
全局函数
(之前已在 GitHub 上的
whatwg/html#793
中讨论过)。它看起来/将看起来像这样:
const clone = structuredClone(original);
在此发布之前,浏览器的结构化克隆实现仅间接公开。
异步解决方法:可用。 😕
使用现有 API 创建结构化克隆的开销较低的方法是通过
MessageChannels
的一个端口发布数据。另一个端口将发出一个
message
事件,其中包含附加的
.data
的结构化克隆。不幸的是,监听这些事件必然是异步的,而同步替代方案不太实用。
class StructuredCloner {
constructor() {
this.pendingClones_ = new Map();
this.nextKey_ = 0;
const channel = new MessageChannel();
this.inPort_ = channel.port1;
this.outPort_ = channel.port2;
this.outPort_.onmessage = ({data: {key, value}}) => {
const resolve = this.pendingClones_.get(key);
resolve(value);
this.pendingClones_.delete(key);
};
this.outPort_.start();
}
cloneAsync(value) {
return new Promise(resolve => {
const key = this.nextKey_++;
this.pendingClones_.set(key, resolve);
this.inPort_.postMessage({key, value});
});
}
}
const structuredCloneAsync = window.structuredCloneAsync =
StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);
示例使用:
const main = async () => {
const original = { date: new Date(), number: Math.random() };
original.self = original;
const clone = await structuredCloneAsync(original);
// They're different objects:
console.assert(original !== clone);
console.assert(original.date !== clone.date);
// They're cyclical:
console.assert(original.self === original);
console.assert(clone.self === clone);
// They contain equivalent values:
console.assert(original.number === clone.number);
console.assert(Number(original.date) === Number(clone.date));
console.log("Assertions complete.");
};
main();
同步解决方法:糟糕!🤢
没有好的选项可以同步创建结构化克隆。这里有几个不切实际的技巧。
history.pushState()
和
history.replaceState()
都创建了它们的第一个参数的结构化克隆,并将该值分配给
history.state
。您可以使用它来创建任何对象的结构化克隆,如下所示:
const structuredClone = obj => {
const oldState = history.state;
history.replaceState(obj, null);
const clonedObj = history.state;
history.replaceState(oldState, null);
return clonedObj;
};
示例用法:
'use strict';
const main = () => {
const original = { date: new Date(), number: Math.random() };
original.self = original;
const clone = structuredClone(original);
// They're different objects:
console.assert(original !== clone);
console.assert(original.date !== clone.date);
// They're cyclical:
console.assert(original.self === original);
console.assert(clone.self === clone);
// They contain equivalent values:
console.assert(original.number === clone.number);
console.assert(Number(original.date) === Number(clone.date));
console.log("Assertions complete.");
};
const structuredClone = obj => {
const oldState = history.state;
history.replaceState(obj, null);
const clonedObj = history.state;
history.replaceState(oldState, null);
return clonedObj;
};
main();
虽然是同步的,但这可能非常慢。它会产生与操作浏览器历史记录相关的所有开销。反复调用此方法可能会导致 Chrome 暂时无响应。
Notification
构造函数
会创建其关联数据的结构化克隆。它还会尝试向用户显示浏览器通知,但除非您已请求通知权限,否则此操作将默默失败。如果您有用于其他目的的权限,我们将立即关闭我们创建的通知。
const structuredClone = obj => {
const n = new Notification('', {data: obj, silent: true});
n.onshow = n.close.bind(n);
return n.data;
};
示例用法:
'use strict';
const main = () => {
const original = { date: new Date(), number: Math.random() };
original.self = original;
const clone = structuredClone(original);
// They're different objects:
console.assert(original !== clone);
console.assert(original.date !== clone.date);
// They're cyclical:
console.assert(original.self === original);
console.assert(clone.self === clone);
// They contain equivalent values:
console.assert(original.number === clone.number);
console.assert(Number(original.date) === Number(clone.date));
console.log("Assertions complete.");
};
const structuredClone = obj => {
const n = new Notification('', {data: obj, silent: true});
n.close();
return n.data;
};
main();