Quiz

Explain the difference between mutable and immutable objects in JavaScript

Topics
JavaScript

TL;DR

Mutable objects allow for modification of properties and values after creation, which is the default behavior for most objects.

const mutableObject = {
name: 'John',
age: 30,
};
// Modify the object
mutableObject.name = 'Jane';
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }

Immutable values cannot be changed after creation. JavaScript objects and arrays are mutable by default; an application can enforce shallow immutability with Object.freeze() or follow an immutable-update convention that creates a new object instead of mutating the existing one.

const immutableObject = Object.freeze({
name: 'John',
age: 30,
});
// Attempt to modify the object
immutableObject.name = 'Jane';
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }

Object.freeze() is shallow, so nested objects remain mutable unless they are frozen separately. Failed writes throw in strict mode and otherwise usually fail silently.


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:

const mutableObject = {
name: 'John',
age: 30,
};
// Modify the object
mutableObject.name = 'Jane';
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }

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:

const immutableObject = Object.freeze({
name: 'John',
age: 30,
});
// Attempt to modify the object
immutableObject.name = 'Jane';
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }

Primitive data types like numbers, strings, booleans, null, and undefined are inherently immutable. Once assigned a value, you cannot directly modify them.

let name = 'Alice';
name.toUpperCase(); // This won't modify the original name variable
console.log(name); // Still prints "Alice"
// To change the value, you need to reassign a new string
name = name.toUpperCase();
console.log(name); // Now prints "ALICE"

Some built-in values are immutable primitives, but common built-in objects such as Date, Map, and Set expose mutating methods. Custom objects and arrays are mutable unless the application constrains them.

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.

Object.freeze() prevents changes to an object's own property descriptors and makes it non-extensible. It does not recursively freeze referenced objects, and it cannot prevent state changes that are not represented by ordinary own properties—for example, calling a mutating method on a frozen Map still changes the map's internal data.

// Using const
const person = { name: 'John' };
person.name = 'Jane'; // Allowed, person.name is now 'Jane'
// person = { name: 'Jane' }; // If uncommented: assignment to a constant
// Using Object.freeze() to protect this object's own properties
const frozenPerson = Object.freeze({ name: 'John' });
console.log(Reflect.set(frozenPerson, 'name', 'Jane')); // false; no change
// frozenPerson = { name: 'Jane' }; // If uncommented: assignment to a constant

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() prevents changes to frozenPerson's own properties.

Object.freeze() creates a shallow frozen object. If it contains nested objects or arrays, those nested data structures remain mutable unless frozen separately.

Therefore, const controls reassignment rather than object mutation. Enforcing deep immutability requires recursively freezing a supported object graph or using an immutable update convention or data structure. Libraries such as Immer and Immutable.js offer different approaches.

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:

const myObject = {};
Object.defineProperty(myObject, 'number', {
value: 42,
writable: false,
configurable: false,
});
console.log(myObject.number); // 42
myObject.number = 43;
console.log(myObject.number); // 42

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(...):

let myObject = {
a: 2,
};
Object.preventExtensions(myObject);
myObject.b = 3;
console.log(myObject.b); // undefined

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.

// Create an object
const person = {
name: 'John Doe',
age: 30,
};
// Seal the object
Object.seal(person);
// Try to add a new property (this will fail silently)
person.city = 'New York'; // This has no effect
// Try to delete an existing property (this will fail silently)
delete person.age; // This has no effect
// Modify an existing property (this will work)
person.age = 35;
console.log(person); // Output: { name: 'John Doe', age: 35 }
// Try to make a non-configurable property enumerable differently.
// Object.defineProperty throws regardless of strict mode.
try {
Object.defineProperty(person, 'name', { enumerable: false });
} catch (error) {
console.log(error.name); // TypeError
}
// Check if the object is sealed
console.log(Object.isSealed(person)); // Output: true

Freezing an object

Object.freeze() creates a frozen object. It makes the object non-extensible, marks all own properties non-configurable, and marks own data properties non-writable. Accessor properties do not have a writable flag, and their setters can still have effects elsewhere.

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).

let immutableObject = Object.freeze({});

Freezing an object prevents adding or removing own properties and restricts changes to their descriptors. It preserves existing enumerability and the prototype, while setting configurability—and, for data properties, writability—to false. It returns the passed object and does not create a copy.

Object.freeze() makes the object immutable. However, it is not necessarily constant. While Object.freeze prevents modifications to the object itself and its direct properties, nested objects within the frozen object can still be modified.

let obj = {
user: {},
};
Object.freeze(obj);
obj.user.name = 'John';
console.log(obj.user.name); //Output: 'John'

What are the pros and cons of immutability?

Pros

  • Easier change detection with immutable updates: If every change produces a new reference and unchanged branches are reused, referential equality can cheaply reveal which branches may have changed. Two separately created immutable objects with equal contents are still not equal by reference.
  • 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.
  • Safer sharing model: Immutable values avoid mutation races in environments that can share memory. Web workers usually exchange ordinary objects through structured cloning rather than sharing the same object; shared memory uses SharedArrayBuffer and requires explicit synchronization.
  • 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 returned 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 their 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 use 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.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

What does this code log?

const settings = Object.freeze({
theme: 'dark',
profile: { compact: false },
});
settings.profile.compact = true;
console.log(settings.theme, settings.profile.compact);