Two worlds: primitives and objects
Let’s start with a necessary premise, just to refresh our memory. In JS, values are divided into two main categories:
Primitives: string, number, boolean, null, undefined
Objects: not only object literals, but also arrays, functions, Date, Set...
The difference is not just academic: it completely changes how they are passed and what kind of side effects you can expect.
Pass by Value
Let’s start with the simplest one: pass by value. When you assign or pass a primitive, you’re actually passing a copy of its value, not the original:

The variable b is assigned the value of a, but this doesn’t mean that if b is changed, the modification will also reflect on a. In fact, b receives a copy of the value of a, not the physical memory address where a’s value resides.
In practice, any changes to variables passed by value have no impact on the outer scope where they are used.
Pass by Reference
With objects, things work differently. Here you don’t copy the object, but rather the memory address where it resides.

Oh! I unexpectedly changed the property value of obj1 as well!!
Disgusting, right? Yes, and this is one of the reasons many developers dislike Javascript.
What’s really happening?
When we write obj2 = obj1, we are assigning obj2 a copy of the memory address of obj1:

This means we are actually referring to the same object in memory, but with a different variable name. Any modification in obj1 will inevitably be reflected in obj2 regardless of the scope, and vice versa. In short, changes to objects passed by reference propagate to the outer scope.
What if we modify the object inside a function?
I’ve got bad news for you:

Even objects passed as function parameters are not safe from this problem, if their properties are directly modified inside the function.
What happens here is that inside the function we dive into the object at the passed memory address and mutate one of its values directly.

obj1 undergoes the direct mutation, and obj2 is also affected even though it wasn’t explicitly modified, unlike obj1.
What if we reassign the object passed as a parameter?
The situation looks similar but is fundamentally different when it comes to reassignment:

Here nothing happens to the original. Why? Inside the function you create a new object with a new address. The original one remains untouched.

And what happens to the reference of the object passed as a parameter? Nothing, it isn’t overwritten: it’s simply ignored inside the function, while the new obj created within will live only in that function’s scope and will be destroyed at the end of its execution.
Important note
A variable name is not an alias of a memory address. obj1 is not a shortcut for “0xA23b”: it’s just a label pointing to that address. When you create a new object, the label can easily point elsewhere. It’s not that we use the reference’s address instead of variable names: we are simply changing what happens in that memory cell.
How to avoid side effects?
As we’ve seen, objects in JS are mutable. This provides the flexibility that defines the language, but often at a cost too high for large applications. However, this can be mitigated. Over the years, several methods have been developed to make objects immutable:
shallow clone (Object.assign, spread operator)
deep clone (structuredClone, _cloneDeep from Lodash, WeakMap)
- Object.freeze(obj)
ES6 const
readonly in Typescript
But we’ll explore these in another article 🙂