Why Do Weird Things Happen When I Clone Objects In JavaScript?

Learn what makes objects different from other data types and how to deal with them the right way

Why Do Weird Things Happen When I Clone Objects In JavaScript?

We've all been there. You need to copy an array or an object into a new one in a new variable. It seems easy enough, just assign the first variable as the value of the second one. But then, when you start to mess with the copy, weird bugs show up, because the original, which was meant to stay put, is suddenly changing too.

I mean, you've always done it this way with numbers and strings and it worked just fine!

const num1 = 1;
const num2 = var1;

console.log(var1 === var2); // prints true, because 1 === 1

const obj1 = { fruit: 'apple' };
const obj2 = { fruit: 'apple' };

console.log(obj1 === obj2); // prints false BUT WHY THOUGH???

GODDAMMIT JAVASCRIPT! WHY DO YOU HATE ME SO MUCH????

What happened here is pretty straightforward: the value attributed to num1 was copied to num2. These are two different variables with two separate values, even though they're the same. This will happen whether you're using numbers, strings, booleans, BigInts, and so on.

Well, you might say, DUH!! I don't care about obvious statements, Mr. Obvious! Help me with my objects already!

I know, I know, but bear with me. I need you to understand a few things for us to move on because they're going to be important. Let's talk about...

Primitive and Non-Primitive Types

You see, JavaScript separates types into two categories: primitive types and non-primitive types. All data types in JavaScript are primitive, except for objects. This means numbers, strings, booleans, BigInts, and even null and undefined are in this category.

They're meant to be fast and lightweight, because they're used all the time, from the most complex of calculations to the most trivial. But in turn, they sacrifice flexibility. Once declared, they can't be changed, they're immutable (values declared with let don't change, they're just replaced by new values), and there are no methods you can apply to these values.

Things change when we're talking about objects. Maybe you're feeling sorry for objects being alone in their own type category, but don't be. That's because, in JavaScript, everything that isn't a primitive value is an object. At its most basic level, objects are just collections of key-value pairs, but on closer inspection, it's a fundamental part of JavaScript and a foundational piece of many of the language's features. Arrays are objects. Functions are objects. Every built-in method is tied to an object. Yeah, objects are powerful, baby!

PrimitivesObjects
ImmutableMutable
FastFlexible
Fixed sizeCan change size freely
Pass by valuePass by reference

But maybe the most important difference between primitive types and objects is how JavaScript stores them in memory. It's high time we talk about...

Value and Reference

When a primitive value is stored in a variable, that variable links to the value itself. Every time you access that variable (like passing it to a function, for instance), that value is copied, and the original value doesn't change. That's what we call "passing by value".

It's a little different for objects, though. Variables don't store objects but references to them. This is called "passing by reference", and means that the variable is actually storing a simple address (the "reference") to the object in memory. JavaScript doesn't allow access to memory addresses, but if it did, you would see something like a 0x followed by a big random number, like this: 0x6533645.

In summary, it's like NFTs in a blockchain, but with a purpose.

This simple difference in storage is what makes objects so special. Because objects aren't fixed to that one place in memory where they were initially stored, they become much more flexible. They can get properties and data added, removed or changed at will. They can have methods you can use to manipulate them however you want. They can even pretend to be primitive types!

💡
When you use a built-in method to manipulate a primitive (like converting a string to a number, for instance), what you're actually doing is making JavaScript temporarily treat that value as an object. You can use typeof to figure out whether a variable is a primitive or an object. Additionally, every data type has a valueOf() and a toString() method.

It's not all fun and games, though, because with great flexibility comes great complexity, as Uncle Ben once said, probably.

Because variables store only references to objects, simple tasks like comparing and copying values don't work the same way.

const obj1 = { fruit: 'apple' };
const obj2 = obj1

console.log(obj1 === obj2); // prints true because they store 
                            // the same reference 

const obj3 = { fruit: 'banana' };
const obj4 = { fruit: 'banana' };

console.log(obj1 === obj2); // prints false because they store
                            // different references

The consequence of the first example above is that changing the values in obj2, obj1 is also affected because they're essentially pointing to the same object. Not being aware of this can lead to all sorts of hard-to-find bugs.

Yeah, it sucks, but don't worry. Now that you understand the mechanics behind all those pesky bugs you see every day, it's time to learn how to deal with them.

Cloning Objects

There are several ways to clone objects in JavaScript. Let's look at some of them.

Object.assign()

The Object.assign() method accepts one object as the target and an indefinite number of objects as sources. It will copy the contents of the source objects into the target.

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }
console.log(user.name); // John
console.log(user.canView); // true
console.log(user.canEdit); // true

Spread syntax

The spread operator (...) is a syntactic sugar for accessing the elements of an array or the properties of an object without needing to type each one of them. The elements are "spread" in the new object or array. This is probably the simplest and easiest way to clone objects and arrays.

const obj = { a: 1, b: 2, c: 3 };
const copy = { ...obj };

console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(copy); // { a: 1, b: 2, c: 3 }

console.log(obj === copy); // false because they don't
                           // have the same reference

So far so good! These cloning techniques are probably good enough for most of your use cases, but before you show me those beautiful wide smiles of yours, know that there's a catch: these are all shallow copies.

Shallow copies are copies that have new references in the top layer of an object, but any lower layer will still be linked to the original. In simpler terms, primitives in the object will be cloned, while objects and arrays inside the object will still be passed by reference.

const obj1 = { fruits: ['bananas', 'strawberries', 'coconuts'] };
const obj2 = { ...obj1 };

console.log(obj1); // { fruits: ['bananas', 'strawberries', 'coconuts' ] }
console.log(obj2); // { fruits: ['bananas', 'strawberries', 'coconuts' ] }

console.log(obj1 === obj2); // false

obj2.fruits.pop();
console.log(obj1); // { fruits: ['bananas', 'strawberries' ] }
                   // because the reference to the array was still
                   // the same for both objects

WHAT!?!?, I can hear you mentally shout, SERIOUSLY?!?! WHAT DO I DO NOW? CAN I EVER KNOW PEACE IN THIS EARTH!?

Don't fret, kiddo! I didn't show you all the techniques there are yet!

structuredClone()

I believe it's a newer feature, but it's already well-supported across browsers and Node.Js. It uses a deep cloning algorithm that's unavailable in all other methods!

const user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

const clone = structuredClone(user);

console.log( user.sizes === clone.sizes ); // false, different objects

// user and clone are totally unrelated now
user.sizes.width = 60;    // change a property from one place
console.log(clone.sizes.width); // 50, not related

It also supports circular references! Circular references are when a property references the object itself, either directly or through a chain of references (this can be useful when dealing with complex data structures like graphs).

const user = {};
// let's create a circular reference:
// user.me references the user itself
user.me = user;

const clone = structuredClone(user);
console.log(clone.me === clone); // true

There's a little caveat, though: structuredClone() doesn't support functions. Trying to clone an object with a function inside will result in an error.

Yeah, nothing's perfect. At that point, your choices are to either circumvent the problem with some code or use _.deepClone() from Lodash.

There you have it. Now you're ready to go clone all the objects you can find in the wild! Go out there and build your own clone army! Palpatine would be proud!

But before you go, a little extra:

Comparing objects

Just like cloning objects, you'll eventually see yourself in a situation where you need to check whether the contents of two objects are all the same. Because they're not directly comparable with ==, you can already see the mess of code you'd have to write for this simple and mundane task.

But here's a simple hack. It only takes one line:

const obj1 = { fruits: ['bananas', 'strawberries', 'coconuts'] };
const obj2 = { fruits: ['bananas', 'strawberries', 'coconuts'] };

console.log(JSON.stringify(obj1) == JSON.stringify(obj2)); // true, because
                                                           // the objects 
                                                           //are now strings

That's right! We just turn the objects into two big strings and compare them! Voilà!

That's it for today! I hope you have a little more love for objects now. Happy coding!