DEV Community

loading...
Cover image for Deep vs Shallow Copy - with Examples

Deep vs Shallow Copy - with Examples

laurieontech profile image Laurie Originally published at tenmilesquare.com ・4 min read

I wrote a post a couple of weeks back about the spread operator.


I noted that you could use it to copy both arrays and objects. However, there was one thing I didn't mention (and should have).

These copies are "shallow". That means that they copy all of the values in the original object. However, if those values are references to other data structures, things get a bit tricky.

I figured the best way to illustrate this was to show a bunch of different examples! Hopefully, this will help explain the potential benefits (and limitations) of the way the spread operator copies.

Note that none of these examples include circular references or repeated keys, etc. There are a number of different problems you can encounter, but we're focused on the "ideal" use cases.

Arrays

This is a straight forward example of copying a flattened array.

let arr = [ 1, 2, 3 ]
let copy = [...arr]
arr.push(4)
// arr is [ 1, 2, 3, 4 ]
// copy is [ 1, 2, 3 ]
Enter fullscreen mode Exit fullscreen mode

If we add an element to arr, copy is unaffected. The same would be true of arr if we added an element to copy.

Now, what happens if our array includes an element that's an object?

let arr = [ 1, 2, {'a': 3} ]
let copy = [...arr]
arr[2]['a'] = 5
// arr is [ 1, 2, {'a': 5} ]
// copy is [ 1, 2, {'a': 5} ]
Enter fullscreen mode Exit fullscreen mode

We can still use the spread operator to copy. However, this introduces some potentially problematic behavior. If we change the contents of the object, it affects both the original array and the copy. The object is copied by reference, so it is shared by both arr and copy.

What about a multi-dimensional array?

let arr = [ 1, 2, [3, 4] ]
let copy = [...arr]
arr[2][0] = 4
// arr is [ 1, 2, [ 4, 4 ] ]
// copy is [ 1, 2, [ 4, 4 ] ]
Enter fullscreen mode Exit fullscreen mode

This ends up being the same example as the one above. The array is copied by reference and thus shared by both arr and copy.

So multi-dimensional arrays can't be changed? Well, not exactly.

let arr = [ 1, 2, [3, 4] ]
let copy = [...arr]
arr[2] = [ 1, 2 ]
// arr is [ 1, 2, [ 1, 2 ] ]
// copy is [ 1, 2, [ 3, 4 ] ]
Enter fullscreen mode Exit fullscreen mode

In this example, even though we have a multi-dimensional array, we're altering it at the top level. So that only affects arr and not copy. Even though the [3,4] was shared, a new array [1,2] was created and referenced by arr instead. So we're not making any changes to the contents of [3,4], we're only removing the reference to it in arr.

Objects

Let's look at how this behavior affects objects. This first example shows what happens when copying a flat object.

let obj = {a:1, b:2, c:3}
let copy = {...obj}
obj['d'] = 4
// obj is {a:1, b:2, c:3, d:4}
// copy is {a:1, b:2, c:3}
Enter fullscreen mode Exit fullscreen mode

As with our array, these two objects are unique clones of each other.

What about a nested object?

let obj = {a:1, b:2, c: {a:1}}
let copy = {...obj}
obj['c']['a'] = 5
// obj is {a:1, b:2, c: {a:5}}
// copy is {a:1, b:2, c: {a:5}}
Enter fullscreen mode Exit fullscreen mode

Again, we see similar behavior to our array examples up top. The nested object is "shared" and any changes to it will be manifested in both top level objects, obj and copy.

So what does this all mean?

As it turns out, "deep copy" is entirely based on whether or not your original structure is more than one level deep. If it's a flattened array, or a flat object structure, the spread operator works just fine for creating a clone.

The "problem" arises if you're referencing another data structure inside your array or object. Those are copied by reference, and changes to them affect all "copies".

How to get a deep copy

So what happens if you want to "deep copy"? Well, you don't want the spread operator!

For a multi-dimensional array you can do this.

let arr = [ 1, 2, [3, 4] ]
var copy = JSON.parse(JSON.stringify(arr))
copy[2][0] = 1
// copy is [ 1, 2, [ 1, 4 ] ]
// arr is [ 1, 2, [ 3, 4 ] ]
Enter fullscreen mode Exit fullscreen mode

Even if the array references an object it will work!

let arr = [ 1, 2, {'a': 3} ]
var copy = JSON.parse(JSON.stringify(arr))
arr[2]['b'] = 4
// arr is [ 1, 2, { a: 3, b: 4 } ]
// copy is [ 1, 2, { a: 3 } ]
Enter fullscreen mode Exit fullscreen mode

Conclusion

Deep and shallow copies can be a confusing concept if you're always working with flattened data structures. Hopefully, these examples allow you to better understand what those terms mean.

If you're looking for other content like this check out the posts below.

Discussion (16)

pic
Editor guide
Collapse
mattdevio profile image
Matt G • Edited

Using JSON.stringify / parse to deep clone objects can get you into trouble with certain data types. Use a library for deep cloning like lodash clonedeep. There is a good Stack Overflow post on this here

Collapse
dinsmoredesign profile image
Derek D

Indeed. Stringify causes all kinds off problems with Dates.

Collapse
laurieontech profile image
Laurie Author

You're correct! But for the examples I put in the post it's a good option. Nice to have these additional links as well.

Collapse
juliang profile image
Julian Garamendy • Edited

Hi! Thank you for writing this!
I came across some unexpected results the other day. It may be worth sharing:

const arr = [1,2,3];
arr[10] = 11; // (why would anyone do this is beyond me)

arr.forEach((n,i) => console.log(i,n))
// 0 1
// 1 2
// 2 3
// 10 11

arr.slice().forEach((n,i) => console.log(i,n))
// 0 1
// 1 2
// 2 3
// 10 11

[...arr].forEach((n,i) => console.log(i,n))
// 0 1
// 1 2
// 2 3
// 3 undefined
// 4 undefined
// 5 undefined
// 6 undefined
// 7 undefined
// 8 undefined
// 9 undefined
// 10 11
Collapse
laurieontech profile image
Laurie Author • Edited

Oh interesting. I just played around with this a bit. And found this.

> let arr = [1,2,3]
> arr[10] = 11
11
> arr
[ 1, 2, 3, <7 empty items>, 11 ]

So it makes sense that the spread syntax would copy that same array.

According to the spec, forEach elides missing array items, so all of those undefined elements won't be accounted for. Why it doesn't in the final case I don't know. Will have to come back and look into a bit.

Thanks for the example!

Collapse
juliang profile image
Julian Garamendy

I just didn't know that arr.slice() and [...arr] are not equivalent.

Here's another example, without forEach.

arr.slice() vs [...arr]

const arr = [{name:'A'},{name:'B'},{name:'C'}]
arr[10] = {name:'D'}

console.log(arr.slice().map(obj => obj.name)) // ["A", "B", "C", empty × 7, "D"]
console.log([...arr].map(obj => obj.name)) // Uncaught TypeError: Cannot read property 'name' of undefined
Thread Thread
laurieontech profile image
Laurie Author

So this is what I see

let arr = [1,2,3]
> arr[5] = 1
> arr.slice()
[ 1, 2, 3, <2 empty items>, 1 ]
> [...arr]
[ 1, 2, 3, undefined, undefined, 1 ]

But no docs are telling me why that's the case. Still searching because I genuinely want to know!

Thread Thread
laurieontech profile image
Laurie Author • Edited

So the difference is holes in an array versus undefined elements. And that happens due to this:

Also explained this way:

Thread Thread
juliang profile image
Julian Garamendy

Awesome! Thank you!

Collapse
cecilelebleu profile image
Cécile Lebleu

This is a great write up! Thank you for sharing. I do have a question though.
I don’t understand the example about nested objects; why is copy in the end still the same as the original obj? I feel that with the accompanying explanation maybe the snippet is wrong, but I don’t know much about how these details work; would you mind explaining this one a bit further?

let obj = {a:1, b:2, c: {a:1}}
let copy = {...obj}
obj['c']['a'] = 5
// obj is {a:1, b:2, c: 5}
// copy is {a:1, b:2, c: {a:1}}
Collapse
laurieontech profile image
Laurie Author

I wrote out a full explanation and the realized the snippet is indeed wrong! Thanks for catching that.

Collapse
cecilelebleu profile image
Cécile Lebleu

Thanks for clearing it out!

Collapse
stereobooster profile image
stereobooster

cursed code

function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}

source dassur.ma/things/deep-copy/

Collapse
bagwaa profile image
Richard Bagshaw

Great read, this stuff is really handy to know when testing with something like Jest as well ..

Collapse
savagepixie profile image
SavagePixie

Very interesting and helpful explanation. Thanks!

Collapse
laurieontech profile image
Laurie Author

So glad it was helpful!