Learn To Clone Like A Sith Lord
Adam Nathaniel Davis
Posted on April 17, 2020
[NOTE: The cloning utilities discussed in this article are now in their own NPM package. You can find them here: https://www.npmjs.com/package/@toolz/clone]
I'm going to highlight the strengths-and-weaknesses of "native" methods for cloning objects/arrays. Then I'm going to show how to create a custom, recursive approach that will faithfully clone ALL THE THINGS.
In most programming languages, objects (and their nephews, arrays) are passed by reference. This is an incredibly useful (and powerful) concept that can be leveraged to do all sorts of impressive things. But one instance where it can feel like a hindrance is when we need to get a full, fresh, clean, standalone copy of an object/array. In other words, there are times when you want a full-fledged clone of an object/array. But this process is not exactly "straight forward".
Tricky References
The simplest version of an object might look something like this:
const phantomMenace = { master: 'palpatine', apprentice: 'maul' };
One of the first gotchas that new devs run into is when they try to "copy" the object, like this:
const phantomMenace = { master: 'palpatine', apprentice: 'maul' };
const attackOfTheClones = phantomMenace;
attackOfTheClones.apprentice = 'dooku';
console.log(phantomMenace.apprentice); // dooku(!)
Code like this is a common source of confusion. Just by giving it a quick read-through, it's easy to come to the (mistaken) conclusion that phantomMenace
and attackOfTheClones
are each independent entities. Continuing with this (flawed) logic, it's tempting to think that console.log(phantomMenace.apprentice);
will output 'maul'
, because the value was set to 'maul' in the phantomMenace
object, and it was only set to 'dooku'
on the attackOfTheClones
object, and not on the phantomMenace
object.
Of course, the reality's quite different. attackOfTheClones
is not a standalone entity. Instead, it's nothing but a pointer referring back to the original phantomMenace
object. So when we update the contents of attackOfTheClones
, the change is also reflected in phantomMenace
.
For this reason, it can sometimes be desirable to have a true, clean, standalone copy of an object/array. An entity that has all the same information as its source - but will act independently after we've copied it. In other words, sometimes we need a full clone of an object/array.
Spread Operators
One very fast, very easy way to clone objects is with the new(ish) spread operator. That would look like this:
const phantomMenace = { master: 'palpatine', apprentice: 'maul' };
const attackOfTheClones = {...phantomMenace};
attackOfTheClones.apprentice= 'dooku';
console.log(phantomMenace.apprentice); // maul
This is so simple that it's tempting to throw out all of your "old" object-cloning tools in favor of spread operators. Unfortunately, this is only "simple" when the object you're cloning is simple. Consider this slightly-more-complex example:
const phantomMenace = {
master: 'palpatine',
apprentice: 'maul',
henchmen: {
one: 'nute gunray',
two: 'rune haako',
},
};
const attackOfTheClones = {...phantomMenace};
attackOfTheClones.henchmen.one = 'jar jar binks';
console.log(phantomMenace.henchmen.one); // jar jar binks(!)
We're back to the original problem. We "cloned" phantomMenace
. Then we made a change to attackOfTheClones
. And then the change was reflected in the original phantomMenace
object. Why did this happen?
The problem occurs because all objects are passed by reference, not just the parent object. In the example above, there are two objects - one nested inside the other.
Using the spread operator, a brand new object was created as attackOfTheClones
. However, when the spread operator was doing its magic, it encountered another object when it reached the henchmen
key. So it copied that object over by reference. This brings us right back to square one.
Theoretically, you can address this problem by doing this:
const phantomMenace = {
master: 'palpatine',
apprentice: 'maul',
henchmen: {
one: 'nute gunray',
two: 'rune haako',
},
};
const attackOfTheClones = {
...phantomMenace,
henchmen: {...phantomMenace.henchmen},
};
attackOfTheClones.henchmen.one = 'jar jar binks';
console.log(phantomMenace.henchmen.one); // nute gunray
But this solution is far-from-scaleable. We can't use attackOfTheClones = {...phantomMenace}
with universal confidence that it will "just work". We have to manually reconfigure our use of the spread operator every time we're dealing with a multilevel object. Yech... And if our object has many nested layers, we need to recreate all those layer with many nested spread operators. Many nested Yechs...
JSON.parse(JSON.stringify())
This is the solution that I've used for all of my "lightweight" object/array cloning. It uses JSON serialization/de-serialization to break the "connection" between a copied object and its source object. JSON.stringify()
converts it into a plain-ol' string - with no knowledge of the originating object. (Because strings are passed by value, not by reference.) JSON.parse()
converts it back into a full-fledged JavaScript object, that still bears no connection to the originating object.
This approach looks like this:
const phantomMenace = {
master: 'palpatine',
apprentice: 'maul',
henchmen: {
one: 'nute gunray',
two: 'rune haako',
},
};
const attackOfTheClones = JSON.parse(JSON.stringify(phantomMenace));
attackOfTheClones.henchmen.one= 'jar jar binks';
console.log(phantomMenace.henchmen.one); // nute gunray
It has some strong features in its favor:
It maintains scalar data types. So if a value was a Boolean, or a number, or
NULL
before it was copied, the cloned version will have those same data types.It's perfectly fine if the source object contains other objects (or arrays).
It's inherently recursive. So if your source object has 100 nested layers of objects, those will be fully represented in the cloned object.
So is this the ultimate answer?? Umm... not really. I leverage this technique on a fairly regular basis, but it fails entirely when you have more "complex" items in your object.
Consider this example:
const phantomMenace = {
master: 'palpatine',
apprentice: 'maul',
henchmen: {
one: 'nute gunray',
two: 'rune haako',
fearLeadsTo: () => console.log('the dark side'),
},
};
const attackOfTheClones = JSON.parse(JSON.stringify(phantomMenace));
console.log(attackOfTheClones.henchmen.fearLeadsTo());
Oops.
The console tells us Uncaught TypeError: attackOfTheClones.henchmen.fearLeadsTo is not a function
. This happens because functions don't survive the serialization process. This is a pretty big gotcha because most modern JavaScript frameworks - like React - are heavily based upon the idea that our objects can contain functions.
There's another nasty problem with this approach that presents itself in React. It comes up when you try to do this:
export default function StarWars() {
const phantomMenace = { key: <Prequel1/>};
const attackOfTheClones = JSON.parse(JSON.stringify(phantomMenace));
return <div>A long time ago, in a galaxy far far away...</div>;
}
This example won't even compile. It throws an error that reads TypeError: Converting circular structure to JSON
. Explaining exactly why that happens would require an entirely new post. Just suffice it to say that you can't serialize React components. And in a large enough app, it's not uncommon to find that you occasionally have objects that contain React components.
Third-Party Cloning Tools
Obviously, I'm not the first person to ponder these challenges. And there are a number of NPM utilities that will allow you to get a deep clone of an object or an array. I don't have any "problem" with such utilities. I'm not going to review them all here. You can have fun googling all those solutions on your own. Some of them are quite good.
But one of my pet peeves is when we import all sorts of outside packages/libraries to do something in JavaScript that we could easily do on our own with plain ol' programming. The reason why most people don't code this up on their own is because, to do it properly, you need to use recursion. And recursion feels to many devs like... the dark side.
Cloning the Sith Way
If we want to "clone like a Sith lord", there's no way that I know to accomplish it without going to the dark side. In other words, we must utilize recursion. Since every object/array can contain a theoretically-endless number of nested objects/arrays, we can't get by with a simple for/while loop. We need something that has the ability to call itself. This isn't "hard". But it steps outside of some devs' comfort zones.
First, let's create a decent test object that will ensure our cloning utilities will truly rise to the task. I'll be using this:
const original = {
one: '1',
two: '2',
nest1: {
four: '4',
five: '5',
header: <SiteHeader/>,
nest2: {
seven: '7',
eight: '8',
function1: () => console.log('the function'),
},
nest3: [
{
john: 'doe',
mary: 'mack',
},
{
butcher: 'brown',
karen: 'conroy',
},
<AnotherComponent/>,
],
},
};
This is a fairly robust object. We have objects inside objects. We have an array inside a (nested) object. We have a function inside one of the nested objects. We have a React component inside one of the nested objects. We have another React component inside the nested array.
First, I want a convenient way to test whether something is an object or an array. To do that, I'm going to use my is()
utility. I wrote about that here:
https://dev.to/bytebodger/javascript-type-checking-without-typescript-21aa
Second, the logic for recursively cloning an object is slightly different than the logic for recursively cloning an array. So I'm going to create two separate, but interdependent, functions.
The code looks like this:
const cloneArray = (originalArray = []) => {
const suppressError = true;
if (!is.anArray(originalArray))
return;
return originalArray.map(element => {
if (React.isValidElement(element))
return element; // valid React elements are pushed to the new array as-is
if (is.anObject(element, suppressError))
return cloneObject(element); // push the CLONED object to the new array
if (is.anArray(element, suppressError))
return cloneArray(element); // push the CLONED array to the new array
return element; // if it's neither an array nor an object, just push it to the new array
});
};
const cloneObject = (originalObject = {}) => {
const suppressError = true;
if (!is.anObject(originalObject))
return;
let clonedObject = {};
Object.keys(originalObject).forEach(key => {
const currentValue = originalObject[key];
if (React.isValidElement(currentValue))
clonedObject[key] = currentValue; // valid React elements are added to the new object as-is
else if (is.anObject(currentValue, suppressError))
clonedObject[key] = cloneObject(currentValue); // set this key to the CLONED object
else if (is.anArray(currentValue, suppressError))
clonedObject[key] = cloneArray(currentValue); // set this key to the CLONED array
else
clonedObject[key] = currentValue; // if it's neither an object nor an array, just set this key to the value
});
return clonedObject;
};
Notice that when we're drilling through an object/array, and we find another object/array, we need to (again) call cloneObect()
or cloneArray()
. This ensures that we keep calling cloneObject()
or cloneArray()
until we finally reach an object/array that has no child objects/arrays. In other words, we have to do this recursively.
So let's put this to the test:
const original = {
one: '1',
two: '2',
nest1: {
four: '4',
five: '5',
header: <SiteHeader/>,
nest2: {
seven: '7',
eight: '8',
function1: () => console.log('the function'),
},
nest3: [
{
john: 'doe',
mary: 'mack',
},
{
butcher: 'brown',
karen: 'conroy',
},
<AnotherComponent/>,
],
},
};
const clone = cloneObject(original);
original.nest1.nest2.eight = 'foo';
console.log(clone);
clone.nest1.nest2.function1();
This passes the test. Merely by calling cloneObject()
, we created a true, deeply-nested clone of the original object.
The cloning process throws no errors. The function sitting at clone.nest1.nest2.function
has survived the cloning process and can be called directly as part of clone
. The React components that were in original
are now transferred over to clone
and can be used in any standard way you would expect to use a React component. Even though we made a subsequent change to original.nest1.nest2.eight
, that change is not reflected in clone
.
In other words: clone
is a true, deep clone of original
, reflecting the exact state of original
at the time we created the clone (but not reflecting any future changes that were made to original
).
Also, by leveraging two inter-dependent functions, there's no need to start the cloning process with an object. If you need to clone an array, you can call cloneArray()
, and that should work the same way, even if the array has many, complex, nested layers - and even if some of those layers consist of objects.
Posted on April 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.