关于 JavaScript 中的深度复制对象有很多讨论,并且在 StackOverflow 上提供了许多英文版的解决方案。我什至有我自己的。
但在我再次承担这项任务之前,我想问一个问题——原则上是否可以自主克隆任意对象?许多解决方案(包括我的最新版本)没有考虑到:
- 具有“函数”类型值的属性
- Getter 和 Setter
- 不可枚举的属性
- Maps 和 Sets(更不用说 WeakMap 和 WeakSet)
我知道这条信息不仅被专业人士阅读,而且还被那些不了解问题深度的初学者阅读,因此可能很想回答这样的问题:“哈!是的,它是初级的!const clone = { ...original }
-就是这样! !!”。但如果就这么简单,我在 Web 开发的第七年就不会问这个问题了,所以让我们分解并清除流行的误解。
但首先,让我们澄清定义。 深拷贝一个对象意味着更改克隆对象的任何属性,包括其所有子对象的属性,都不会以任何方式影响原始实例,而克隆对象与原始实例完全相同,包括属性函数,非- 可枚举的属性,等等。
错误决策的分析与排除
序列化后反序列化
在这个方法中,我们首先用 序列化对象,JSON.stringify(original)
然后用 反序列化它JSON.parse
。在一行中,它将是这样的:
JSON.parse(JSON.stringify(original))
让我们尝试克隆以下原始对象:
const original = {
alpha: "FOO",
bravo: () => { return "BAR" }
}
const clone = JSON.parse(JSON.stringify(original));
但是如果我们将它打印到控制台,那么bravo属性将会丢失。此外,这种方法对我们来说不再感兴趣。
Object.assign 和传播运算符
查看语法
const clone = { ...original }
或者
const clone = Object.assign({}, original )
不做深度克隆——事实上,这是用一个容器替换另一个容器,内容引用与原始对象相同的内存区域。
const original = {
alpha: "FOO",
bravo: {
charlie: "BAR"
}
}
const clone = { ...original };
clone.bravo.charlie = "HOGE";
console.log(original.bravo.charlie); // "HOGE"!!! Какого ... ?!
对于对象的特殊情况 - 数组也是如此:
const clone = [ ...original ];
好吧,正如 Dan Abramov 在他的 Redux 中所建议的那样,分别复制每个子对象是 21 世纪第三个十年的某种狂野——当我被提议做时,我可以谈论什么样的纳米技术、机器人技术和火星地球化这么日常的工作?这就是这种日常废话的样子:
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
深度复制任意对象的问题
复制功能
首先,尚不清楚函数的深度克隆实际上意味着什么。从技术上讲,这可能意味着声明与现有函数相同的函数,但在不同的内存区域中。但也许我错过了一些东西。
我的职业生涯是从 ES6 语法开始的,所以我没有发现程序员使用函数作为类来敲钉子的时候。如果不考虑这些技巧,那么
function original() {
}
const clone = original;
虽然不会是深拷贝,但它会导致任何问题吗?
我在英语的 StackOverflow 上找到了这个解决方案:
const newFunc = oldFunc.bind({});
如果这样的解决方案不会引起任何副作用,那么我会注意到它。
一种或另一种方式,我们在这里谈论对象的深度克隆,特别是属性-函数(方法)。在那里,在方法内部,也可以有this
,并且在深度复制期间,必须将链接指向this
克隆。
复制 getter 和 setter
当对 getter 和 setter 进行迭代时Object.entries(original)
(如我上面的解决方案),值将是这些 getter 和 setter 返回的值(并且由于 setter 本质上不返回任何内容,这将是undefined
):
const original = {
alpha: "FOO",
get charle() {
return this.alpha + "__" + "BAR";
},
set delta(value) {
this.alpha = value;
}
}
for (const [key, value] of Object.entries(original)) {
console.log(key);
console.log(value);
}
此处属性的值charle
将是字符串FOO__BAR
。
alpha
FOO
charle
FOO__BAR
delta
undefined
它不会像这样工作(会有相同的结果):
for (const key of Object.keys(original)) {
// ...
}
不可枚举的属性
说实话,根据我的所有经验,我几乎从不需要迭代不可枚举的属性。但原则上,我们可以了解不可枚举属性 * 的存在(例如,通过Object.getOwnPropertyNames(object1)
),甚至更多地转向它们,因此更进一步 - 上述所有问题。
地图和集合
Map 无法转换为 JSON,这可能会导致问题(我不确定我之前建议的解决方案是否可以通过改进来最终确定;可能需要从根本上改变方法)。
也许这些集合需要通过创建一个相同类型的新容器来单独处理。但毕竟,随着时间的推移,新的容器会被添加到 ES6 中,然后你将不得不对实现进行更改
最后一个问题
问题是:上述所有与派生对象的深度克隆有关的问题,以及我没有提及的问题,原则上是可以克服的吗?在创建深拷贝功能时可以考虑到这一点吗?或者,仅自主复制仅包含可序列化为 JSON 的数据类型(数字、字符串、布尔值、null、数组、对象)的对象是否现实?
通常不可能创建一个完全独立的对象。
从新的 - 无法以任何方式读取或传输的私有属性:
从旧 - 所有关闭:
并且不复制原型、函数绑定 getter 和其他一切都无济于事。