Object copy using spread syntax actually shallow or deep?
Asked Answered
P

4

39

I understand spread syntax makes a shallow copy of objects, i.e., the cloned object refers to the same reference as the original object.

However, the actual behaviour seems contradicting and confusing.

const oldObj = {a: {b: 10}};

const newObj = {...oldObj};

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

Above behaviour makes sense, newObj is also updated by updating oldObj because they refer the same location.

const oldWeirdObj = {a:5,b:3};

const newWeirdObj = {...oldWeirdObj};

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

I don't understand, why is newWeirdObj not updating similar to oldWeirdObj?

They are still referring to the same location if I am not wrong, but why is the update to oldWeirdObj not updating newWeirdObj?

Pied answered 25/4, 2020 at 6:35 Comment(3)
Does this answer your question? Does Spreading create shallow copy?Sadiesadira
@norbitrial, So it means these terms( shallow and deep copy ) applies only to nested non primitive data types ?Like nested objects ?Pied
primitive value vs reference value developer.mozilla.org/en-US/docs/Web/JavaScript/…Banjermasin
C
90

So, for this problem, you have to understand what is the shallow copy and deep copy.

Shallow copy is a bit-wise copy of an object which makes a new object by copying the memory address of the original object. That is, it makes a new object by which memory addresses are the same as the original object.

Deep copy, copies all the fields with dynamically allocated memory. That is, every value of the copied object gets a new memory address rather than the original object.

Now, what a spread operator does? It deep copies the data if it is not nested. For nested data, it deeply copies the topmost data and shallow copies of the nested data.

In your example,

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

It deep copy the top level data, i.e. it gives the property a, a new memory address, but it shallow copy the nested object i.e. {b: 10} which is still now referring to the original oldObj's memory location.

If you don't believe me check the example,

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;}

You see the c property at the newObj is untouched.

How do I deep copy an object.

There are several ways I think. A common and popular way is to use JSON.stringify() and 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;}

Now, the newObj has completely new memory address and any changes on oldObj don't affect the newObj.

Another approach is to assign the oldObj properties one by one into newObj's newly assigned properties.

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;} 

There are some libraries available for deep-copy. You can use them too.

Cog answered 25/4, 2020 at 7:26 Comment(14)
Thanks much, i got the conceptPied
Copying by spread is a shallow copy. const objnew = objold is not a copy at all, just a new reference to the same object. Copying the object keys and values but not deeper is a shallow copy by definition. The second deepcopy approach isn't scalable and highly unpractical.Selfish
@Sajeeb Ahamed you explained well. But I don't think this approach JSON.parse(JSON.stringify(oldObj)) is any good. Since it has various downsides, it can be dangerous while using without considering them. It will remove your getter, setter, class prototype, keys with undefined, Symbol, function values. Turn your Date object into string, NaN into null. Turn your Set, Map into regular Object etc. And speaking of performance, this performs worst. #48494850Renvoi
You are right @Imran. This is just a way I've just approached. But this is not the best way to do it.Cog
Can anyone please suggest scalable and practical approach to this problem?Huckaby
@BhushanPatil that's why i like immutable-js lib. You can easily compare deep structures, any change is not causing side-effects in any case etc. immutable-js.github.io/immutable-js there is method fromJS which will create immutable-js List/Object from JS array/mapJeannettajeannette
In the first example const oldObj = {a: {b: 10}} if it's copied as such const newObj = {...oldObj} Does this actually mean that the property a is now stored in a new memory address; however, the value held inside that memory address is the address of old object i.e., {b: 10}. And if it were deep copied the value inside a would be a new memory address pointing to a new {b: 10}. Please correct me if I'm wrong.Phoenix
But if this is the case, we could also say that it deep copies primitives and shallow copies objects, the behaviour is same with arrays and spread as it is with objects and spreadPhoenix
@Phoenix yeah you could say that. And array is also an object in javascript so applied the same conditions for the array and object.Cog
I noticed this statement "it deep copies the data if it is not nested" all over the internet, but it's nonsense. Spread operator does a simple shallow copy, period. The problem is that people probably don't understand the difference between a primitive value and a reference to an object in C-like languages like C++, C#, Java and JavaScript. If the data is not "nested" (meaning you only have primitive values), it's still a shallow copy. If the data is "nested" (meaning some of the items are objects), it's still a shallow copy.Thorwald
@Groo for the example const obj = {a: {b: 20}} the property obj.a is not a primitive value but an object reference. But if I perform an operation like const newObj = {...obj} the newObj.a will get a new memory address. So, isn't it copied the top level (non-primitive value) deeply? Correct me if I am wrong.Cog
newObj will get a new memory address, but newObj.a and oldObj.a will have the same reference (pointing to the same object in memory), which can be confirmed with newObj.a === oldObj.a (jsfiddle.net/7nuL84hs). This is commonly known as shallow copy, i.e. you create a clone of the original object in memory, but all other objects referenced by this object are not copied.Thorwald
Technically you are right. :+1:Cog
@SajeebAhamed and Hardwork, thanks for your input. I had read too many articles saying that the top level properties are also "deep copied" which made me think the cloned object and its top level properties that were objects all had new addresses. But I finally understand that is not the case.Astatine
N
7

structuredClone() is a new global method for real deep clones, all nested elements of the copy will have new memory addresses.

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' } }
Nomism answered 17/2, 2023 at 18:34 Comment(1)
Looks like the exact answer we need in 2024, note that it has full browser compatability - developer.mozilla.org/en-US/docs/Web/API/…Stagg
D
5

Spread syntax effectively goes one level deep while copying an array. Therefore, it may NOT be suitable for copying multidimensional arrays, as the following example shows. (The same is true with 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]]

original docs

Dray answered 22/5, 2022 at 20:14 Comment(0)
M
4

Primitive typed values are copied by values, whereas objects typed values are copied by reference. In your second example both properties are primitive. that's why it's a deep copy (values were copied), but in the first example, the property is an object; hence it's a shallow copy (reference were copied).

Spread Operator

First you should understand how the spread operator really works. If the oldObj is printed as it's, then as you see the array is printed as the value assigned to it, but if you put oldObj inside of an object, then it's printed as '{oldObj: '}.

Now let's use the spread operator to oldObj while using it inside of an object, then as you see the key:value pair was taken out, and inserted into the outside object.

So this is what spread operator does. It's quite easy to understand it with arrays than objects. It removes elements from inside array, object, and insert them into the outside array, object. In array, if you use ...[1,2,3], it comes as 1,2,3, meaning elements were all removed from the array, and available as individual elements. With objects, you can't use as ...{key:'value'} but you can use as {...{key:'value'}} as then the key:value pair is inserted into the outer object ( {key:'value'} ) after removing from the inner object.

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

Your Example

In the second example, as I mentioned earlier, oldObj key:value pair was removed from the inner object, and included in the outer object; hence you get the same object.

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

The thing in here is a: has an object typed value; hence it's copied by REFERENCE, meaning when you copy oldObj to newObj, you copy the reference. That's why when you change oldObj.a.b, it affects to the newObj too.

//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}}

In the third example, both the values of the a, b properties are primitive types. When primitive typed values are copied, you literally make a deep copy (copy values not reference). When you made a deep copy, and make a change to the old value, it doesn't affect to the new one, because both these properties are located in different locations of the memory.

//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}

If you want to copy by VALUES, then you have to copy each values instead of objects, as following. What happens in this example is, you assign {a: {b: 10}} to oldObj constant, then If you want to referer to property 'a' to access its value, you have to use as 'oldObj.a'. So you use the spread operator to it to take its value out, which is a primitive data typed value (10), then you use {a : X } as this to make a new object. Basically it goes like this a : {...{b: 10}} -> a : {b: 10}, meaning you get the same object with a deep copy. Now if you make a change to oldObj.a.b, it only affects to the old object, not the new one.

//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}}
Madgemadhouse answered 21/12, 2020 at 22:22 Comment(1)
It always does a shallow copy. What does it even mean to "deep copy a primitive value"? It's a value, you copy it, there is no other way of copying it.Thorwald

© 2022 - 2024 — McMap. All rights reserved.