by

Why is it so hard to copy an object in JavaScript

Reading Time: 6 minutes

Have you ever sat down to write a bit of JavaScript thinking, “this is easy; shouldn’t take more than a few minutes,” and then six hours later there you are with a bottle of whisky in one hand and a sharpened stick in the other shouting curses in Latin at StackOverflow answers?

Obviously that’s a rhetorical question because all web developers have at some point learned the hard way how to copy objects in JavaScript.

Let’s talk about why it’s hard and weird and what we can do about it (that doesn’t involve invoking ancient curses (probably))

sipping a cup of Java making some copies
Todd thought it was a funny prank asking the new hire to make some copies of Koko’s Kitten. He had no idea the the new hire was a clone.

You don’t copy an object by reassigning variablesdiligently deny demons dairydiligently deny demons dairy

At some point in your JavaScript journey, you might be tempted to think that you can copy an object just by assigning it to a new variable, like so:

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux'
};

const clone = frank;

Yeah that’s all well and good. And then you’ll modify the copy:

clone.name.first = "Bizarro Frank";

And everything is fine, so long as you never go back and look at the original variable:

screenshot of a console where we see that assigning an object to a new variable doesn't copy the object. Modifying the new variable has modified the original.
Swearing is a natural response. Just let it out.

It’s totally natural to think, “oh, I’ll just assign this variable again,” and then believe, “I’ve copied it.”

What’s really hard is that this is not what JavaScript thinks. JavaScript has different plans for you.

JavaScript passes variables by value

Let’s do some experiments, shall we?

Nothing weird about strings, right?

const thread = "some string";
let fabric = thread;

console.log(fabric); // "some thread"
fabric = "some yarn";
console.log(fabric, thread); // "some yarn"
  • strings are immutable
  • All we did was assign fabric to a new value

Nothing weird about numbers, right?

const digit = 42;
let integer = digit;

console.log(integer); // 42

integer++;

console.log(digit, integer); // 42, 43
  • The variables each referred to a specific number
  • When we incremented integer, we made it reference a new number value

What about booleans?

const veracity = true;
let maybe = veracity;

console.log(maybe); // true

maybe = false;

console.log(veracity, maybe); // true, false
  • The variables each referred to a specific boolean value
  • When we reassigned maybe, we made it reference a new boolean value

It’s about the thing you’re referring to

Let’s repeat our experiment with an array:

const nouns = ['person', 'place'];
let moreNouns = nouns;

console.log(nouns); // ['person', 'place'];

moreNouns.push('thing');

console.log(nouns, moreNouns); // ['person', 'place', 'thing'], ['person', 'place', 'thing'];
  • the variables each referred to a specific value, which was an array
  • So moreNouns referred not to nouns, but to ['person', 'place']
  • So when we updated moreNouns, we updated the array to which it was referring

You don’t get a new object when you make a new variable because you don’t get a new house when your street name changes. They’re both just references to a location.

Koko, from her lesser-known fourth book, “Why I sharpened this stick”

But then how do you copy a friggin’ object in JavaScript?

Object.assign, obviously

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux'
};

const clone = Object.assign({}, frank);

clone.handle = "xuaecap";

console.log(frank.handle, clone.handle); // 'Paceaux', 'xuaecap';

Problem solved, right? Just use Object.assign(), make the first argument an empty object, and the second object the original.

Case closed, problem solved, let’s go home.

Our problems have only begun

Object.assign has a teeny tiny lil’ pitfall (or two (or three)).

a monkey gleefully sipping a cup of coffee at a copy machine
After the last incident, management thought for sure Todd wasn’t going to be trying any more shit. Todd had just moved on to the coffee machine.

Object.assign doesn’t help with nested objects

Let’s repeat our little experiment, but this time on the name object.

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux'
};

const clone = Object.assign({}, frank);

clone.name.first = "Knarf";

console.log(frank.name.first, clone.name.first); // 'Knarf', 'Knarf';
  • We copied property values
  • if a property value is an object, what we copy is a reference to the object
  • So frank.name and clone.name actually both have the same reference to the same object

Object.assign is an agent of chaos if you’re using getters and setters

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux',
 get fullname() {
  return `${this.name.first} ${this.name.last}`;
 }
};

const clone = Object.assign({}, frank);

clone.name.first = "Knarf";

// pay very close attention to what you're about to see next:


console.log(frank.fullname); // 'Knarf Taylor'
console.log(clone.fullname); // 'Frank Taylor'

Time to start swearing some more

  • We copied property values
  • We copied a reference to an object
  • Object.assign doesn’t copy your fancy dynamic getters, it copies their values
    • so the clone got the computed value for fullname, which was “Frank Taylor”
  • Meanwhile the original object still had a dynamic its dynamic getter
    • And both variables refer to the same name object, which we updated
    • So the original object returns “Knarf Taylor” for fullname

Object.create can solve the problem with copying getters and setters

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux',
 get fullname() {
  return `${this.name.first} ${this.name.last}`;
 }
};

const clone = Object.create(frank);

clone.name.first = "Knarf";

console.log(frank.fullname, clone.fullname); // 'Knarf Taylor', 'Knarf Taylor';
  • Still copied those property values
  • Still copied a reference to an object
  • Object.create copied the dynamic getter, though
    • So the clone didn’t get the computed value, but the dynamic getter
  • Meanwhile, the original object is still the original
    • Both variables refer to the same name object, which we updated
    • So now the original and the clone both return “Knarf Taylor” for fullname

And then there’s all the other problems with Object.assign

  • Anything that’s non-enumerable isn’t copied
  • anything that’s on the prototype chain is also ignored
  • Any properties set to writable: false will throw an exception that interrupts the copy task

So Object.assign is only good for plain-jane ordinary ol’ objects that are one level deep.

So how are you supposed to copy an object with nested objects?

Yeah so there’s copying, and then there’s deep copying.

We’re talking about deep copying.

Handle it as JSON

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux'
};

const clone = JSON.parse(JSON.stringify(frank));

clone.name.first = "Knarf";

console.log(frank.name.first, clone.name.first); // 'Frank', 'Knarf'
  • We stringified the object
  • We then converted that string back into an object (i.e. we created a new object)
    • Which means that frank.name and clone.name reference totally different objects

The only problem here is the one you’re already thinking of…

How does the JSON stringify thing work if you have dynamic getters and setters?

const frank = {
 name: {
  first: 'Frank',
  last: 'Taylor'
 },
 handle: 'Paceaux',
 get fullname() {
  return `${this.name.first} ${this.name.last}`;
 }
};

const clone = JSON.parse(JSON.stringify(frank));

clone.name.first = "Knarf";

console.log(frank.fullname, clone.fullname); // 'Frank Taylor', 'Frank Taylor';

Now is when you need to get up from your computer and go hug a loved one.

  • We stringified the object
  • So stringification means we copied the computed value
    • Which means we copied the result of fullname into the clone

Sooo… the JSON trick maybe isn’t great for dynamic getters and setters.

And this is why it’s so hard to copy an object in JavaScript

The StackOverflow answers on this aren’t exactly fun reads. You can practically hear the desperation in the answers and feel the very sharp sticks they’ve all crafted from chair legs. Every answer has gotchas that essentially amount to:

  • JavaScript references by value in variable assignments and object assignments (i.e. objects nested on objects)
  • See above

You see, getters and setters are this kind of internal function (a type of description) that lives on an object.

And functions are objects.

And as we’ve definitely figured out by now, JavaScript points to references of objects not their values.

But there is hope*

There now exists structuredClone, which is exactly what you think it is. It’s a deep cloning function.

The catch, though, is that it’s not part of the JavaScript specification. It’s part of the DOM. So if you want to use it in a non-browser environment, you’re going to need a polyfill.

Of course, you could also use a Lodash library or a good ol’ fashioned NPM package.

(Almost) No one will judge you.

Gorilla wearing a backwards trucker hat at a terminal.
Todd calling the other clones “troops” was the last straw

Sources and whatnots