克隆 JavaScript 对象的最有效方法是什么?我见过有人obj = eval(uneval(o));
在使用,但这是非标准的,仅受 Firefox 支持。
我做过类似obj = JSON.parse(JSON.stringify(o));
但质疑效率的事情。
我还看到了具有各种缺陷的递归复制函数。
我很惊讶不存在规范的解决方案。
在 JavaScript 中深度克隆对象的最有效方法是什么?
原生深度克隆
它被称为“结构化克隆”,在 Node 11 及更高版本中进行实验性工作,并有望登陆浏览器。有关更多详细信息,请参阅此答案。
数据丢失的快速克隆 - JSON.parse/stringify
如果不使用Date
S,功能,undefined
,Infinity
,正则表达式,地图,集合,斑点,的文件列表,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 的回答。
使用库进行可靠的克隆
由于克隆对象并非易事(复杂类型、循环引用、函数等),大多数主要库都提供了克隆对象的函数。不要重新发明轮子- 如果您已经在使用库,请检查它是否具有对象克隆功能。例如,
- 洛达什 -
cloneDeep
; 可以通过lodash.clonedeepmodule单独导入,如果您还没有使用提供深度克隆功能的库,它可能是您的最佳选择 - AngularJS -
angular.copy
- jQuery -
jQuery.extend(true, { }, oldObject)
;.clone()
只克隆 DOM 元素 - 只是图书馆 -
just-clone
; 只做一件事的零依赖 npm module库的一部分。适用于各种场合的无罪公用事业。
ES6(浅拷贝)
为了完整起见,请注意 ES6 提供了两种浅拷贝机制:Object.assign()
和扩展语法。它将所有可枚举属性的值从一个对象复制到另一个对象。例如:
var A1 = {a: "2"};
var A2 = Object.assign({}, A1);
var A3 = {...A1}; // Spread Syntax
检查这个基准:http : //jsben.ch/#/bWfk9
在我之前的测试中,我发现速度是主要问题
JSON.parse(JSON.stringify(obj))
是深克隆最慢的方式的对象(它是慢于jQuery.extend与deep
标志设置由10-20%真)。
当deep
标志设置为false
(浅克隆)时,jQuery.extend 非常快。这是一个不错的选择,因为它包含一些用于类型验证的额外逻辑,并且不会复制未定义的属性等,但这也会稍微减慢您的速度。
如果您知道要克隆的对象的结构或者可以避免深层嵌套数组,则可以编写一个简单的for (var i in obj)
循环来在检查 hasOwnProperty 的同时克隆您的对象,它会比 jQuery 快得多。
最后,如果您试图在热循环中克隆一个已知的对象结构,您可以通过简单地内联克隆过程并手动构建对象来获得更多的性能。
JavaScript 跟踪引擎在优化for..in
循环方面很糟糕,检查 hasOwnProperty 也会减慢你的速度。当速度是绝对必须的时,手动克隆。
var clonedObject = {
knownProp: obj.knownProp,
..
}
当心JSON.parse(JSON.stringify(obj))
在Date
对象上使用该方法-JSON.stringify(new Date())
以 ISO 格式返回日期的字符串表示形式,它JSON.parse()
不会转换回Date
对象。有关更多详细信息,请参阅此答案。
此外,请注意,至少在 Chrome 65 中,本地克隆不是可行的方法。根据 JSPerf 的说法,通过创建新函数来执行本机克隆比使用 JSON.stringify 慢近800 倍, JSON.stringify 在整个过程中都非常快。
如果您使用 Javascript ES6,请尝试使用这种本地方法进行克隆或浅拷贝。
Object.assign({}, obj);
假设您的对象中只有变量而不是任何函数,您可以使用:
var newObject = JSON.parse(JSON.stringify(oldObject));
结构化克隆
2021更新:在structuredClone
全局函数即将登陆浏览器,Node.js的,和杰诺。
HTML 标准包括一个内部结构化克隆/序列化算法,可以创建对象的深层克隆。它仍然仅限于某些内置类型,但除了 JSON 支持的少数类型之外,它还支持日期、正则表达式、地图、集合、Blob、文件列表、图像数据、稀疏数组、类型化数组,以及将来可能更多. 它还保留克隆数据中的引用,使其支持可能导致 JSON 错误的循环和递归结构。
Node.js 中的支持:实验性 🙂
在structuredClone
全球功能将很快被Node.js的提供:
const clone = structuredClone(original);
在那之前:v8
Node.js 中的module当前(从 Node 11 开始)直接公开结构化序列化 API,但此功能仍标记为“实验性”,并且在未来版本中可能会更改或删除。如果您使用的是兼容版本,则克隆对象非常简单:
const v8 = require('v8');
const structuredClone = obj => {
return v8.deserialize(v8.serialize(obj));
};
浏览器的直接支持:即将推出 🙂
在structuredClone
全局函数很快就会被所有主流浏览器提供(前面已经讨论WHATWG / HTML#793 GitHub上)。它看起来/看起来像这样:
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;
};
使用示例:
虽然是同步的,但这可能会非常慢。它会产生与操纵浏览器历史记录相关的所有开销。重复调用此方法可能会导致 Chrome 暂时无响应。
该Notification
构造函数创建其相关数据的结构化克隆。它还尝试向用户显示浏览器通知,但除非您请求通知权限,否则这将静默失败。如果您有其他用途的许可,我们将立即关闭我们创建的通知。
const structuredClone = obj => {
const n = new Notification('', {data: obj, silent: true});
n.onshow = n.close.bind(n);
return n.data;
};
使用示例:
如果没有任何内置的,你可以尝试:
function clone(obj) {
if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
return obj;
if (obj instanceof Date)
var temp = new obj.constructor(); //or new Date(obj);
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;
}