今天,在调查我们应用程序中的错误时,我目睹了 JavaScript
structuredClone
中非常令人惊讶的行为。
此方法承诺创建给定值的深度克隆。这是之前使用
JSON.parse(JSON.stringify(value))
技术实现的,在纸面上 structuredClone
似乎是该技术本身的超集,产生相同的结果,同时还支持日期和循环引用等内容。
但是,今天我了解到,如果您使用
structuredClone
克隆一个包含指向同一引用的引用类型变量的对象,这些引用将被保留,而不是使用不同的引用创建新值。
这是一个演示此行为的玩具示例:
const someSharedArray = ['foo', 'bar']
const myObj = {
field1: someSharedArray,
field2: someSharedArray,
field3: someSharedArray,
}
const myObjCloned = structuredClone(myObj)
console.log(myObjCloned)
/**
{
"field1": ["foo", "bar"],
"field2": ["foo", "bar"],
"field3": ["foo", "bar"],
}
**/
myObjCloned.field2[1] = 'baz'
// At this point:
// Expected: only `field2`'s value should change, because `myObjCloned` was deeply cloned.
// Actual: all fields' values change, because they all still point to `someSharedArray`
console.log(myObjCloned)
/**
{
"field1": ["foo", "baz"],
"field2": ["foo", "baz"],
"field3": ["foo", "baz"],
}
**/
这是
structuredClone
非常令人惊讶的行为,因为:
JSON.parse(JSON.stringify(value))
在我们的代码库中,我们总是使用
structuredClone
,但鉴于这一新发现,我正在考虑将所有 structuredClone
用法更改为 JSON.parse(JSON.stringify(value))
,以防止意外结果。
不是真正的深复制?
它是深拷贝。
正确的深拷贝应满足以下几个条件:
它应该将原始文件中的每个不同对象映射到结果中的一个不同对象:一对一映射。这也是确保支持循环引用的指导原则。
如果两个不同的属性具有相同的值 (
Object.is(a, b) === true
),那么深度克隆中的这些属性也应该彼此相同。
在您的示例输入中有两个不同的对象:一个数组和一个(顶级)复杂对象。更进一步,
Object.is(myObj.field1, myObj.field2)
的结果是true。
您在示例中使用
structuredClone
得到的内容遵循这一点。值得注意的是,Object.is(myObjCloned.field1, myObjCloned.field2)
是正确的。
你期望得到什么(以及
JSON.parse(JSON.stringify(value))
返回什么)违反了这个原则:将创建三个不同的数组,这意味着same数组已被复制多次,并且不存在1对1不再映射了。前面提到的 Object.is
表达式的计算结果为 false。
让我们输入带有反向引用的输入:
const root = {};
root.arr = [root, root, root];
这里我们有一个对象和一个数组。后者包含对第一个对象的三个引用。同样在这里,我们期望对一个对象的这三个引用会产生另外三个引用,每个引用都引用唯一的克隆父对象。这与您的示例中发生的原理相同,只是共享引用恰好是父对象。