Explain the difference between mutable and immutable objects in JavaScript
TL;DR
Mutable objects allow for modification of properties and values after creation, which is the default behavior for most objects.
Immutable objects cannot be directly modified after creation. Its content cannot be changed without creating an entirely new value.
The key difference between mutable and immutable objects is modifiability. Immutable objects cannot be modified after they are created, while mutable objects can be.
Immutability
Immutability is a core principle in functional programming but it has lots to offer to object-oriented programs as well.
Mutable objects
Mutability refers to the ability of an object to have its properties or elements changed after it's created. A mutable object is an object whose state can be modified after it is created. In JavaScript, objects and arrays are mutable by default. They store references to their data in memory. Changing a property or element modifies the original object. Here is an example of a mutable object:
Immutable objects
An immutable object is an object whose state cannot be modified after it is created. Here is an example of an immutable object:
Primitive data types like numbers, strings, booleans, null
, and undefined
are inherently immutable. Once assigned a value, you cannot directly modify them.
Some built-in immutable JavaScript objects are Math
, Date
but custom objects are generally mutable.
const
vs immutable objects
A common confusion / misunderstanding is that declaring a variable using const
makes the value immutable, which is not true at all.
const
prevents reassignment of the variable itself, but does not make the value it holds immutable. This means:
- For primitive values (numbers, strings, booleans),
const
makes the value immutable since primitives are immutable by nature. - For non-primitive values like objects and arrays,
const
only prevents reassigning a new object/array to the variable, but the properties/elements of the existing object/array can still be modified.
On the other hand, an immutable object is an object whose state (properties and values) cannot be modified after it is created. This is achieved by using methods like Object.freeze()
which makes the object immutable by preventing any changes to its properties.
In the first example with const
, reassigning a new object to person
is not allowed, but modifying the name
property is permitted. In the second example, Object.freeze()
makes the frozenPerson
object immutable, preventing any changes to its properties.
It's important to note that Object.freeze()
creates a shallow immutable object. If the object contains nested objects or arrays, those nested data structures are still mutable unless frozen separately.
Therefore, while const
provides immutability for primitive values, creating truly immutable objects requires using Object.freeze()
or other immutability techniques like deep freezing or using immutable data structures from libraries like Immer or Immutable.js.
Various ways to implement immutability in plain JavaScript objects
Here are a few ways to add/simulate different forms of immutability in plain JavaScript objects.
Immutable object properties
By combining writable: false
and configurable: false
, you can essentially create a constant (cannot be changed, redefined or deleted) as an object property, like:
Preventing extensions on objects
If you want to prevent an object from having new properties added to it, but otherwise leave the rest of the object's properties alone, call Object.preventExtensions(...)
:
In non-strict mode, the creation of b
fails silently. In strict mode, it throws a TypeError
.
Sealing an object
Object.seal()
creates a "sealed" object, which means it takes an existing object and essentially calls Object.preventExtensions()
on it, but also marks all its existing properties as configurable: false
. Therefore, not only can you not add any more properties, but you also cannot reconfigure or delete any existing properties, though you can still modify their values.
Freezing an object
Object.freeze()
creates a frozen object, which means it takes an existing object and essentially calls Object.seal()
on it, but it also marks all "data accessor" properties as writable:false, so that their values cannot be changed.
This approach is the highest level of immutability that you can attain for an object itself, as it prevents any changes to the object or to any of its direct properties (though, as mentioned above, the contents of any referenced other objects are unaffected).
Freezing an object does not allow new properties to be added to an object and prevents users from removing or altering the existing properties. Object.freeze()
preserves the enumerability, configurability, writability and the prototype
of the object. It returns the passed object and does not create a frozen copy.
Object.freeze()
makes the object immutable. However, it is not necessarily constant. Object.freeze
prevents modifications to the object itself and its direct properties, nested objects within the frozen object can still be modified.
What are the pros and cons of immutability?
Pros
- Easier change detection: Object equality can be determined in a performant and easy manner through referential equality. This is useful for comparing object differences in React and Redux.
- Less complicated: Programs with immutable objects are less complicated to think about, since you don't need to worry about how an object may evolve over time.
- Easy sharing via references: One copy of an object is just as good as another, so you can cache objects or reuse the same object multiple times.
- Thread-safe: Immutable objects can be safely used between threads in a multi-threaded environment since there is no risk of them being modified in other concurrently running threads. In the most cases, JavaScript runs in a single-threaded environment
- Less memory needed: Using libraries like Immer and Immutable.js, objects are modified using structural sharing and less memory is needed for having multiple objects with similar structures.
- No need for defensive copying: Defensive copies are no longer necessary when immutable objects are returning from or passed to functions, since there is no possibility an immutable object will be modified by it.
Cons
- Complex to create yourself: Naive implementations of immutable data structures and its operations can result in extremely poor performance because new objects are created each time. It is recommended to use libraries for efficient immutable data structures and operations that leverage on structural sharing.
- Potential negative performance: Allocation (and deallocation) of many small objects rather than modifying existing ones can cause a performance impact. The complexity of either the allocator or the garbage collector usually depends on the number of objects on the heap.
- Complexity for cyclic data structures: Cyclic data structures such as graphs are difficult to implement.