DEV Community

Cover image for Deep Copy and the Immutability Issue
Sam Beckham
Sam Beckham

Posted on • Originally published at sam.beckham.io

Deep Copy and the Immutability Issue

In the latest episode of, "I Have No Idea What I'm Doing" I learned that everything I thought I knew about immutability in Javascript was a lie.

Okay, so I'm being dramatic. Not everything was a lie. But a fundamental part of my understanding was incorrect. After speaking to a few people about the issue, it seems this was a shared misconception.

This all stems from a subtle, yet fundamental difference in how we copy objects in javascript. Deep copying and shallow copying.

Deep copying is what we want for true immutable data. It is a copy of all the values of an object, and all the values of all the objects within it. Shallow copying—on the other hand—is a copy of all the values of an object, with references to all the objects within it. This is what tripped me up.

To understand the problem, we need to look at three ways of copying objects.

Referencing

Okay, so let's strip this all the way back. Let's create a mutable reference to an object.

const initialObject = { name: "Sam", twitter: "@samdbeckham" };
const newObject = initialObject;

This is bad for immutability because any changes to newObject reflect in initialObject like so:

newObject.twitter = "@frontendne";
console.log(initialObject.twitter); // @frontendne

In this example, newObject is a reference to initialObject. So whenever we get or set data on either of these objects it is also applied to the other object. This is useful in a lot of different ways, but not great for immutability.

Shallow copying

This is the most common form of copying data in an immutable manner. We utilise the spread operator to create a copy of initialObject. If you've used redux before, you'll have seen this inside your reducers.

const initialObject = { name: "Sam", twitter: "@samdbeckham" };
const newObject = { ...initialObject };

It's a subtle change, but the ... makes all the difference. newObject is no-longer linked to initialObject. It's now a copy of the data and an entirely new object. So if we make the same change we did earlier, we get the following result:

newObject.twitter = "@frontendne";
console.log(initialObject.twitter); // @samdbeckham
console.log(newObject.twitter); // @frontendne

Modifying the data on newObject doesn't affect initialObject anymore. We can go about our day, modifying newObject and initialObject remains clean.

But this is a shallow copy, and the immutability is only one level deep. To show this, we need an object inside our initialObject:

const initialObject = {
  name: "Sam",
  social: {
    twitter: "@samdbeckham",
    youtube: "frontendne"
  }
};
const newObject = { ...initialObject };

At first glance, this newObject looks like an immutable copy of initialObject but look what happens when we do this:

newObject.social.twitter = "@frontendne";

console.log(initialObject.social.twitter); // @frontendne

Sadly, the immutability is only skin deep. As soon as we go down another level, we're back to referencing values. If we were to open up newObject, it would look a bit like this:

const newObject = {
  name: "Sam",
  social: initialObject.social
};

We can get around this issue by shallow copying one level deeper and defining newObject like so:

const newObject = {
  ...initialObject,
  social: { ...initialObject.social }
};

This is how it's usually dealt with in redux, but it only adds one more level of immutability. If there are any other nested objects they will still be stored as references. You can see how (with certain data structures) this could get messy.

Note: Object.assign() and Object.freeze() have the same shallow copying issues as spread.

Deep Copying

Finally, we get to deep copying. Deep copying offers us true object immutability. We can change any value in an object—no matter how deeply nested it is—and it won't mutate the data we copied it from.

const initialObject = {
  name: "Sam",
  social: {
    twitter: "@samdbeckham",
    youtube: "frontendne"
  }
};
const newObject = deepCopy(initialObject);

newObject.social.twitter = "@frontendne";

console.log(initialObject.social.twitter); // @samdbeckham
console.log(newObject.social.twitter); // @frontendne

Hooray! We're immutable!

Unfortunately, Javascript doesn't have a function called deepCopy() so we've had to make our own; and it isn't pretty. There's no "nice" way to handle deep copying in Javascript. Das Surma wrote a article on deep copy which has a few good examples, here are some of the simpler ones.

JSON

This is the most concise and easy to understand method, and it looks like this:

const deepCopy = object => JSON.parse(JSON.stringify(object));

First we turn the object into a JSON string with JSON.stringify() then we convert that string back into an object with JSON.parse(). Stringifying the data throws out all references, making the returned object completely immutable. But, if there are any references we need to keep inside this object, they're gone. If we have any maps, regex, dates, or other special types; they're gone. If we have any cyclic objects inside the object (which we shouldn't) the whole thing breaks and throws an error. So it's not very robust.

Data Laundering

If you don't want want to deal with the issues the JSON parser brings, there are a few—albeit hacky—methods you can can use. These all revolve around pasing our data to a service, then querying that service to pull our cleaned data back out. It's like money laundering, only with data, and nowhere near as cool.

For example, we can utilise the notification API:

const deepCopy = object =>
  new Notification("", {
    data: object,
    silent: true
  }).data;

This triggers a notification, silences it, then returns the data from that notification. Unfortunately, the user needs to be able to receive notifications for this to work.

We can also utilise the history API and the messageChannel in similar ways. But they all have their downsides.

What do now?

Deep copying is a bit of a heavy-handed approach to immutability. Being aware of the gotchas of shallow copying should be enough to see you through most problems. You can use the nested spread method outlined above to fix any problem areas.
If this approach is starting to get unwieldy, you should aim to improve your data structure first.

If you absolutely need deep copying, then fear not. There's an issue on the HTML spec that hopes to address this, with the introduction of structuredClone(). The more visibility this gets, the more likely it is to be implemented. Until then, I'd suggest using a library like Immutable.js to handle your immutability. Or grab the cloneDeep() helper from the underscore library for a quick-fix.

If you're up for a challenge, try coming up with your own solution to deepCopy. My friend Niall had a lot of fun playing with some ideas on Twitter. I'd be interested to see what you all come up with.

This post was originally published on my website

Discussion (10)

Collapse
habilain profile image
David Griffin

I think you're still off on your understanding of Javascripts data model - that or there's a few errors in what you said. The main thing to realize is that in JavaScript you only have references, and that values/objects are never stored inside a variable. Instead the value/object is stored somewhere in memory and the variable points at that bit of memory.

This is significant for any mutable value/object; for immutable values (e.g. strings and integers) there's no difference (and note that when I say strings and integers are immutable I'm only talking about the values; I can assign a reference to 1 to a variable and then reassign that variable to reference a 2 when I increment the variable. I cannot change the idea of 1 across the entire program into a 2.) If a value is mutable however, then having multiple references to the variable and mutating it causes these issues.

That means there's a few implications for what you've said:

1) You seem to imply that a reference creates a new object; it doesn't. They are the same object (i.e. newObject === initialObject), but you've put a reference to the object into two variables. Because they are the same, a modification on the object pointed at by newObject modifies initialObject.

2) With the Shallow copy, you create a copy of the object and its variables, but those variables still point at mutable objects which are shared with the initial object. Mutating those mutable objects causes the contents of the object to change. But if my shallow copy only contained immutable values, this wouldn't be an issue.

3) A deep copy resolves these issues by making it so that in any case there is a mutable object, the mutable object is copied - and hence variables don't point at the same object twice.

Collapse
samdbeckham profile image
Sam Beckham Author

Those three points were exactly the points I was trying to make. I didn’t go in to how the underlying structures work in JavaScript in order to not get too stuck in the weeds. I did try to point out that variables in JavaScript are references unless you explicitly copy the data though. I’m not sure where the error in what I said was though, can you point me to it so I can update any misleading parts?

Collapse
habilain profile image
David Griffin • Edited

I'm not sure if I'm going into how the underlying structures work in any detail - understanding at this level is pretty fundamental to understanding the logic in programming languages like JavaScript which follow this paradigm.

As to an error, it's statements like this: "In this example, newObject is a reference to initialObject. So whenever we get or set data on either of these objects it is also applied to the other object. This is useful in a lot of different ways, but not great for immutability." You've implied that newObject and initialObject are two different objects, when actually they are two references to the same object (and in addition, initialObject isn't an object either; newObject and initialObject just reference the same actual object). There may be a few other places when you've conflated references and objects - I'm currently on a bus and it's hard to type things.

Thread Thread
samdbeckham profile image
Sam Beckham Author

Fair enough. I can see the confusion there. Though It’s a deliberate conflation to make the point easier to understand. It’s not intended to mislead at all.

Thread Thread
habilain profile image
David Griffin

I fear that may be a fools economy, because that conflation perpetuates the misunderstanding that led to you writing this post in the first place!

Thread Thread
samdbeckham profile image
Sam Beckham Author

You could be right. I have a feeling there’s another post in here somewhere.

Collapse
eljayadobe profile image
Eljay-Adobe • Edited

Probably want to have a deepFreeze() as well as a deepCopy(). Can be helpful if one wants to employ an immutability idiom in JavaScript.

BUT...

I have to point out that immutability really isn't part of the JavaScript paradigm.

You see it in languages like D, Haskell, OCaml/F#, and Elm. But in those language it's part of the core paradigm of those languages.

Trying to mimic that kind of immutability -- even though it is a powerful technique -- in JavaScript is tricky. Any other JavaScript programmer looking at your code will be very puzzled as to the rationale for doing that and all the extra logic stitched in at the right places to support immutability idiom.

It's worth it as an educational exercise. I would be very conservative rolling a JavaScript-based immutability idiom out into production code.

Collapse
samdbeckham profile image
Sam Beckham Author

Thanks Elijay, this has been pointed out to me by a few people. I've misused the word, 'immutable' in this post. The data isn't immutable, there's just no link back to the original data.

Also, I'm not trying to suggest using any of these deep copying techniques. It was more to point out that the spread operator didn't do quite what I thought it did.

I may go back and try to firm up these points and change my wording. It's causing a bit of confusion.

Collapse
chiragkamat profile image
Chirag Kamat

We can use lodash deepclone for deepcopying

Collapse
samdbeckham profile image
Sam Beckham Author

Yep. Lodash has a couple of helper functions for this