Hault! If you're hoping for some interesting tidbits on the second Star Wars movie from 20+ years ago...this is not the article you are looking for.
If you're still hear, then perhaps you're hoping to learn a little something about cloning objects in Javascript. If so, you've come to the right place because that is exactly what I want to talk about in this post!
As a Javascript developer, cloning objects has been a bit of an issue for a while. There are many different ways to do it, but you have to know and understand the ins and out and the pros and cons of each to know when to use them and why. But luckily, a browser API has come available in the last year or 2 that alleviates this problem.
You may be wondering, "If this thing has been out for a year or 2, why is this person writing about it now?". That's a great question! The answer...because I still talk to a lot of people who have never heard about it. So this is me trying to spread the word on this little piece of functionality that has made my life better.
So what is this mystical, Jedi-esk power I'm speaking of?
[queue dramatic music]
structuredClone
This new browser API's main purpose is to allow us to quickly and easily create deep clones of objects without the need of some workaround or third party library! Not only that, but it also brings with it the ability to transfer properties from one object to another instead of cloning them.
So why is this new API worth a blog post? To understand that, we first need to understand a few things about Javascript.
Shallow vs Deep Clones
There are actually multiple types of clones in Javascript, and we need to understand these before we can understand why this new API is so valuable. The two types we need to consider are shallow and deep clones.
When we create a shallow clone of an object, it means we're only cloning the top level properties of that object. If the object contains any nested objects, their properties will be copied rather than cloned.
A deep clone, on the other hand, clones all the properties, even the nested ones.
To better understand this, let's say we have an object like this:
const user = {
name: 'Anakin Skywalker',
age: 19,
jedi: {
rank: 'Knight',
master: 'Obi-Wan Kenobi',
},
}
If we were to create a shallow clone, it would look like this:
const user = {
name: 'Anakin Skywalker', // new
age: 19, // new
jedi: { // new
rank: 'Knight', // old
master: 'Obi-Wan Kenobi', // old
},
}
When I say old here, I mean that these values were not cloned, they were only copied.
On the other hand, if we were to create a deep clone of the same object, it would look like this:
const user = {
name: 'Anakin Skywalker', // new
age: 19, // new
jedi: { // new
rank: 'Knight', // new
master: 'Obi-Wan Kenobi', // new
},
}
Okay, we understand the difference between shallow and deep clones, but why does it matter? Shallow, deep, we just want to clone the object and use it in our application, right? Now let's look at the problem...
The Problem
Let say we make a shallow clone of our object and then want to update it. No problem...
const user = {
name: 'Anakin Skywalker',
age: 19,
jedi: {
rank: 'Knight',
master: 'Obi-Wan Kenobi',
},
}
// create the shallow clone
const clone = {...user};
// update the cloned data
clone.jedi.rank = 'Master';
That was easy...let's log out our objects to confirm everything worked...
console.log(user);
\\ {
\\ name: 'Anakin Skywalker',
\\ age: 19,
\\ jedi: {
\\ rank: 'Master',
\\ master: 'Obi-Wan Kenobi',
\\ },
\\ }
console.log(clone);
\\ {
\\ name: 'Anakin Skywalker',
\\ age: 19,
\\ jedi: {
\\ rank: 'Master',
\\ master: 'Obi-Wan Kenobi',
\\ },
\\ }
Uh oh, looks like we have an issue...both of our objects have been updated! But we only changed the value on the clone. How could this be?
The problem stems from how Javascript stores data in memory and how it references that data.
When you assign an object to a variable, you aren't actually assigning the data to it. Instead, you're assigning the address of where that data is stored in memory. Then when you assign that variable's value to another variable, the second variable receives the same address. Now there are 2 different variables that contain the same address so they are both looking at the same data in memory.
To better understand this, let's continue to use our example. Let's imagine that Javascript has stored each value in a different part of memory and the properties don't hold the value, but instead they hold the address to the data in memory...like this:
(note: This is only for simple visualization to understand the topic. Memory management in Javascript is much more complicated than this simple example portrays, but it serves to help understand the difference between reference and value.)
const user = {
name: abc123, // 'Anakin Skywalker'
age: abc124, // 19
jedi: {
rank: abc125 // 'Master'
master: abc126 // 'Obi-Wan Kenobi'
},
}
We can now see that each property is storing an address in memory where each piece of data is located. With this in mind, let's also look at our clone in the same way:
const user = {
name: def123, // 'Anakin Skywalker'
age: def124, // 19
jedi: {
rank: abc125 // 'Master'
master: abc126 // 'Obi-Wan Kenobi'
},
}
Now we can clearly see the differences and similarities between the 2. Remember, our clone is only a shallow clone so only the top level properties were cloned. The rest are copied from the original object.
So if we look at top level properties name
and age
, we can see that new addresses in memory were assigned to them. But the nested properties jedi.rank
and jedi.master
have the same addresses as our original object. This is why both objects printed out the same data when we logged them earlier. By changing the value of clone.jedi.rank
, we were actually changing the data in memory stored at address abc125
. Since both objects are referencing that same address, they both printed out the same data.
Sometimes, when a developer thinks they're cloning an object, they're often only creating a shallow clone, resulting in updates being made to other objects they didn't intend to change. This small nuance has caused a great many bugs over the years...and they can be tricky to track down.
How to make a Shallow Clone
Now that you understand what a shallow clone is, I want to show you 2 methods that create them. So when you see them in the wild, you'll know more clearly what they're doing.
const clone1 = { ...user };
const clone2 = Object.assign({}, x);
Deep Clones...the old way
So we understand what deep clones are, and the need for them, but how do we create them?
Before the structuredClone
API came about, our options were limited, and none were particularly "good". Here are the 2 most common methods I've encountered:
(note: These are not the only 2. There are many others, especially when cloning arrays. I limited to only 2 for the sake of time.)
import { cloneDeep } from 'lodash';
const clone1 = JSON.parse(JSON.stringify(user));
const clone2 = cloneDeep(user);
In the first example, we first stringify the user object, and then immediately parse it back to JSON. The creates an entirely new object from the original one. While this works in many cases, it has some pretty severe drawbacks. One of the most notable issues with this method is that it doesn't handle non-serializable data correctly. If you had things like Functions, DOM Nodes, or Errors in your object, things wouldn't work out as you planned.
Consider this simple example:
const obj = {
update: () => console.log('update'),
};
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone); // {}
Notice how the clone is only an empty object. The update method on obj
was not cloned over. This is one of the most notable issues with using this method to clone objects.
The second example uses lodash's cloneDeep
function. The downside here is that we're dependent on an external library. This means there is more code that has to be loaded to our app in order accomplish the task. Depending on your use case and requirements, this may be a deal breaker, or it may not be as big of a deal.
structuredClone
Finally, let's play with the new hotness we've been gifted.
Making a deep clone with structuredClone
is very simple, just call it like any other function, passing in the object you want to clone.
const clone = structuredClone(user);
Simple!
We no longer have to import a third party library, and if something can't be cloned (like Functions) structuredClone
will throw an exception letting us know!
Browser Support
More good news, structuredClone
is supported in the latest versions of the big 4 browsers (Chrome, Safari, Firefox, and Edge)! For older versions there's a polyfill available as well.
Conclusion
Phew, that was a lot! But we now understand the problems we used to have cloning objects (particularly complex objects) and how structuredClone
is here to make our lives better!
We also saw the difference between shallow and deep clones, the difference between accessing data by reference and by value, and how we used to have to create deep clones, as well as the downsides of those methods.
Thanks for joining me, and until next time, Happy Hacking!