使用扩展语法进行对象复制,实际上是浅拷贝还是深拷贝?

36
我了解扩展语法会对对象进行浅拷贝,也就是说克隆的对象与原始对象引用相同。
然而,实际行为似乎有些矛盾和令人困惑。
const oldObj = {a: {b: 10}};

const newObj = {...oldObj};

oldObj.a.b = 2;
newObj  //{a: {b: 2}}
oldObj  //{a: {b: 2}}

上述行为是有道理的,因为通过更新`oldObj`,也会更新`newObj`,因为它们引用同一个位置。
const oldWeirdObj = {a:5,b:3};

const newWeirdObj = {...oldWeirdObj};

oldWeirdObj.a=2;
oldWeirdObj   //{a:2,b:3}
newWeirdObj   //{a:5,b:3}

我不明白,为什么newWeirdObj没有像oldWeirdObj一样更新?
如果我没记错的话,它们仍然指向同一个位置,但为什么对oldWeirdObj的更新没有影响到newWeirdObj

这个回答解决了你的问题吗?展开操作会创建浅拷贝吗? - norbitrial
@norbitrial,这是否意味着这些术语(浅拷贝和深拷贝)仅适用于嵌套的非原始数据类型?比如嵌套对象? - richa Singh
原始值和引用值 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Data_structures#primitive_values - xgqfrms
4个回答

86
浅复制深复制是需要理解的两个概念。浅复制是对象的位拷贝,通过复制原始对象的内存地址来创建新对象。也就是说,它创建了一个新对象,这个对象的内存地址与原始对象相同。深复制则复制了所有动态分配内存的字段。也就是说,复制对象的每个值都有一个新的内存地址,而不是原始对象的内存地址。
现在,什么是展开运算符?如果数据不是嵌套的,它会深度复制数据。对于嵌套的数据,它将深度复制最上层的数据,并浅复制嵌套的数据。
在您的示例中,
const oldObj = {a: {b: 10}};
const newObj = {...oldObj};

它会深拷贝顶层数据,即它会给属性a一个新的内存地址,但它只会浅复制嵌套对象,例如{b: 10}仍然指向原来的oldObj内存位置。

如果你不相信,请查看示例:

const oldObj = {a: {b: 10}, c: 2};
const newObj = {...oldObj};

oldObj.a.b = 2; // It also changes the newObj `b` value as `newObj` and `oldObj`'s `b` property allocates the same memory address.
oldObj.c = 5; // It changes the oldObj `c` but untouched at the newObj



console.log('oldObj:', oldObj);
console.log('newObj:', newObj);
.as-console-wrapper {min-height: 100%!important; top: 0;}

您可以看到在newObj中 c 属性未被更改。

如何深度复制一个对象。

我认为有几种方法。一种常见且流行的方法是使用JSON.stringify()JSON.parse()

const oldObj = {a: {b: 10}, c: 2};
const newObj = JSON.parse(JSON.stringify(oldObj));

oldObj.a.b = 3;
oldObj.c = 4;

console.log('oldObj', oldObj);
console.log('newObj', newObj);
.as-console-wrapper {min-height: 100%!important; top: 0;}

现在,newObj 拥有全新的内存地址,对 oldObj 的任何更改都不会影响 newObj

另一种方法是逐个将 oldObj 属性赋值给 newObj 的新属性。

const oldObj = {a: {b: 10}, c: 2};
const newObj = {a: {b: oldObj.a.b}, c: oldObj.c};

oldObj.a.b = 3;
oldObj.c = 4;

console.log('oldObj:', oldObj);
console.log('newObj:', newObj);
.as-console-wrapper {min-height: 100%!important; top: 0;} 

有一些可用于深拷贝的库。你也可以使用它们。


5
通过扩展复制的方式进行复制是一种浅拷贝。const objnew = objold其实根本没有进行复制,只是一个指向同一对象的新引用。仅复制对象的键和值而不进行更深层次的复制是按定义来说的浅拷贝。第二种深度复制的方法不可扩展且高度不实用。 - goteguru
4
@Sajeeb Ahamed,您讲解得很好。但是我认为这种方法 JSON.parse(JSON.stringify(oldObj)) 没有什么好处。由于它具有各种缺点,如果在不考虑这些缺点的情况下使用,可能会很危险。它将删除您的 getter、setter、类原型、带有 undefinedSymbol、函数值的键。将您的日期对象转换为字符串,NaN 转换为 null。将您的 SetMap 转换为普通对象等等。说到性能,这个方法表现最差。https://dev59.com/E6jka4cB1Zd3GeqPBJhq - Imran
1
@BhushanPatil 这就是为什么我喜欢immutable-js库。你可以轻松地比较深层结构,任何更改在任何情况下都不会引起副作用等。https://immutable-js.github.io/immutable-js/ 有一个名为fromJS的方法,它将从JS数组/映射创建immutable-js列表/对象。 - Lukas Liesis
2
在第一个例子中,如果像这样复制const oldObj = {a: {b: 10}},即const newObj = {...oldObj},这是否意味着属性a现在存储在新的内存地址中;然而,该内存地址中保存的值是旧对象的地址,即{b: 10}。如果进行深度复制,则a内部的值将是指向新{b: 10}的新内存地址。如果我错了,请纠正我。 - Hardwork
2
我注意到这个语句“如果数据没有嵌套,它会进行深拷贝”在互联网上广泛流传,但这是无稽之谈。扩展运算符只做一个简单的浅拷贝,就这样。问题在于人们可能不理解类C语言(如C++、C#、Java和JavaScript)中原始值和对象引用之间的区别。如果数据不是“嵌套”的(意思是你只有原始值),那么它仍然是浅拷贝。如果数据是“嵌套”的(意思是其中一些项是对象),那么它仍然是浅拷贝。 - vgru
显示剩余9条评论

5

Spread语法只能复制数组的一层内容。因此,它可能不适用于复制多维数组,正如以下示例所示。(同样适用于Object.assign())

let x = [1,2,3];
let y = [...x];

y.shift(); // 1
console.log(x); // 1, 2, 3
console.log(y); // 2, 3

let a = [[1], [2], [3]];
let b = [...a];

b.shift().shift(); // 1

//  Oh no!  Now array 'a' is affected as well:
a
//  [[], [2], [3]]

原始文档


3
原始类型值是按值复制的,而对象类型值是按引用复制的。在第二个示例中,两个属性都是原始类型,这就是为什么它是深层复制(值被复制),但在第一个示例中,属性是一个对象;因此它是浅层复制(引用被复制)。
展开运算符
首先,您应该了解展开运算符的实际工作方式。如果将oldObj打印为它,则会看到数组打印为分配给它的值,但如果将oldObj放入对象中,则会打印为'{oldObj:}'。
现在让我们在使用它时将spread运算符应用于oldObj,并将其放入对象中,然后可以看到键值对已被取出并插入到外部对象中。
所以这就是展开运算符的作用。用数组比对象更容易理解。它从内部数组、对象中移除元素,并将它们插入到外部数组、对象中。在数组中,如果使用...[1,2,3],则会得到1,2,3,这意味着所有元素都已从数组中删除,并可用作单独的元素。对于对象,您不能使用...{key:'value'},但可以使用{... {key:'value'}},然后将键值对插入到外部对象({key:'value'})中,而不是从内部对象中删除。
//Example 1
const oldObj = {a: {b: 10}};
console.log(oldObj); // { a: { b: 2 } }
console.log({oldObj}); // { oldObj: { a: { ... } } }
console.log({...oldObj}); // { a: { b: 2 } }

你的例子

在第二个例子中,正如我之前提到的那样,oldObj的键值对被从内部对象中移除,并包含在外部对象中;因此你得到了相同的对象。

{ oldObj : {a: {b: 10}} } -> {a: {b: 10}} // (with spread operator) 

这里的东西是一个具有对象类型值的变量;因此它是通过引用复制的,也就是说当你将oldObj复制到newObj时,你复制了引用。这就是为什么当你改变oldObj.a.b时,它也会影响到newObj的原因。
//Example 2
const oldObj = {a: {b: 10}};
const newObj = {...oldObj};

oldObj.a.b = 2;
console.log(newObj)  //{a: {b: 2}}
console.log(oldObj)  //{a: {b: 2}}

在第三个例子中,a和b属性的值都是基本类型。当复制基本类型的值时,您实际上进行了深层复制(复制值而不是引用)。当您进行深层复制并更改旧值时,它不会影响新值,因为这两个属性位于内存的不同位置。

//Example 3
const oldWeirdObj = {a:5,b:3};
const newWeirdObj = {...oldWeirdObj};

oldWeirdObj.a=2;
console.log(oldWeirdObj)     //{a:2,b:3}
console.log(newWeirdObj)   //{a:5,b:3}

如果你想通过值进行复制,则必须复制每个值而不是对象,如下所示。在这个例子中发生的是,你将{a: {b: 10}}分配给oldObj常量,然后如果你想引用属性'a'来访问它的值,则必须使用'oldObj.a'。所以你使用展开运算符将其取出它的值,这是一个原始数据类型的值(10),然后你使用{a : X}作为这个新对象。 基本上像这样 a : {...{b: 10}} -> a : {b: 10},意思是你得到了一个深度复制的相同对象。现在,如果你更改oldObj.a.b,它只会影响旧对象,而不是新对象。
//Example 4
const oldObj = {a: {b: 10}};
const newObj = {a: {...oldObj.a}};

oldObj.a.b = 2;
console.log(newObj)  //{a: {b: 10}}
console.log(oldObj)  //{a: {b: 2}}

它始终执行浅复制。 "深度复制原始值" 意味着什么?它是一个值,你复制它,没有其他复制方式。 - vgru

2

structuredClone() 是一个全新的全局方法,用于进行真正的深度克隆,复制品的所有嵌套元素都将具有新的内存地址。

const obj = { a: "a", nestedObj: { b: "b", c: "c" } };
const newObj = structuredClone(obj);

newObj.a = "different a";
newObj.nestedObj.b = "different b";

obj.nestedObj.c = "original c";

console.log(obj);
console.log(newObj);

// output 
//{ a: 'a', nestedObj: { b: 'b', c: 'original c' } }
//{ a: 'different a', nestedObj: { b: 'different b', c: 'c' } }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接