Enforcing Immutability for JavaScript Constants
Introduction
In JavaScript, the const keyword, introduced in ES6, is used to declare constants. A constant's immutability refers to the inability to reassign the variable's memory address. For primitive data types (like numbers, strings, booleans), this means the value itself cannot change. However, for reference types such as objects and arrays, the reference (the memory address) is fixed, but the contents of the referenced data structure can still be modified. This can make constants appear mutable.
The "Mutable" Constant
Memory in JavaScript is divided into the stack and the heap. Primitive values are stored directly on the stack. Reference types are stored on the heap, while a reference pointer (the memory address) to that heap location is stored on the stack.
When you declare a constant object, you are only preventing the reassignment of that reference pointer. The object's properties remain editable.
const userProfile = {
username: 'Neo'
}
userProfile.username = 'Morpheus' // This is allowed
console.log(userProfile) // Output: { username: 'Morpheus' }
Freezing Objects
To make an object truly immutable, you can use Object.freeze(). This method transforms an object so that you cannot add new properties, delete existing ones, or modify the values of existing properties.
const gameSettings = {
difficulty: 'Hard',
graphics: {
quality: 'High',
resolution: '4K'
}
}
// Shallow freeze
Object.freeze(gameSettings)
gameSettings.difficulty = 'Easy' // This change will be ignored in strict mode or silently fail
console.log(gameSettings.difficulty) // Output: 'Hard'
// Nested objects are not frozen
gameSettings.graphics.quality = 'Medium'
console.log(gameSettings.graphics) // Output: { quality: 'Medium', resolution: '4K' }
As shown, Object.freeze() performs a shallow freeze. It does not affect nested objects. To achieve deep immutability, a recursive freezing function is requirde.
Implementing Deep Freeze
Here is an example of a function that recursively freezes an object and all its nested properties.
function deepFreeze(targetObject) {
// Retrieve the property names defined on the object itself
const propNames = Object.getOwnPropertyNames(targetObject)
// Freeze properties before freezing self
for (const name of propNames) {
const value = targetObject[name]
// Recursively call deepFreeze if the property is an object and not already frozen
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value)
}
}
// Freeze the initial object
return Object.freeze(targetObject)
}
const appConfig = {
theme: 'Dark',
preferences: {
notifications: true,
layout: {
sidebar: 'Collapsed'
}
}
}
deepFreeze(appConfig)
appConfig.theme = 'Light' // Fails silently or throws in strict mode
appConfig.preferences.layout.sidebar = 'Expanded' // Also fails
console.log(appConfig.preferences.layout.sidebar) // Output: 'Collapsed'