Immutability in JavaScript - Object.freeze() & Object.seal()

This documentation explores the concept of immutability in JavaScript, focusing on the methods Object.freeze() and Object.seal(). It includes detailed explanations, examples, and real-world use cases to provide a comprehensive understanding for beginners.

Introduction to Immutability

What is Immutability?

Immutability is a core concept in programming that emphasizes creating data structures that cannot be changed after they are created. In simple terms, once an immutable data structure is created, its state cannot be modified. Instead of changing the structure, any operation that would modify the structure returns a new structure with the desired changes.

Imagine you have a blueprint of a house. Once you have finalized the blueprint, you wouldn’t change it. Instead, if you need changes, you create a new blueprint with those changes. This is similar to immutability, where the original structure remains unchanged, and any modifications result in a new structure.

In programming, immutability can lead to more predictable code, fewer bugs, and is particularly useful in scenarios like state management in applications.

Understanding Objects in JavaScript

Basic Object Creation

In JavaScript, objects are king. They allow you to store multiple pieces of data in a single variable. Think of objects as containers that can hold different types of data. Here’s how you can create a simple object:

const person = {
    firstName: "John",
    lastName: "Doe",
    age: 30
};

In this example, we’ve created an object named person that contains three properties: firstName, lastName, and age.

Modifying Object Properties

Adding Properties

You can easily add new properties to an object.

person.job = "Engineer";

This will add a new property job to the person object with the value "Engineer".

Changing Property Values

To change the value of an existing property, you simply assign a new value to it.

person.age = 31;

This changes the age property of the person object from 30 to 31.

Deleting Properties

You can also delete properties from an object using the delete keyword.

delete person.job;

This will remove the job property from the person object.

Concept of Immutability in JavaScript

Why Use Immutability?

Immutability is particularly useful in scenarios where data consistency is crucial. For example, in state management in modern JavaScript frameworks like React or Vuex, immutability helps in managing the state efficiently by avoiding unintended side effects.

Another advantage of immutability is that it makes tracking changes easier. When data cannot be changed, any change leads to the creation of a new data structure, making it easy to track what has changed.

Introducing Object.freeze()

What is Object.freeze()?

Object.freeze() is a method in JavaScript that freezes an object, preventing any modifications to its properties. A frozen object cannot be changed in any way, and attempts to modify it, add, or delete properties will fail silently or throw an error in strict mode.

Syntax

The syntax for Object.freeze() is straightforward:

const object = { key: value };
Object.freeze(object);

How Does it Work?

When you freeze an object using Object.freeze(), the object becomes immutable. This means:

  • Properties Cannot be Changed: The values of existing properties cannot be changed.
  • Properties Cannot be Added: New properties cannot be added to the object.
  • Properties Cannot be Deleted: Existing properties cannot be deleted from the object.

Examples

Example 1: Freezing a Simple Object

Let’s freeze a simple object and try to modify it.

const simpleObject = { name: "Alice", age: 25 };
Object.freeze(simpleObject);

// Trying to change an existing property
simpleObject.name = "Bob"; // No effect in non-strict mode
console.log(simpleObject); // Output: { name: "Alice", age: 25 }

// Trying to add a new property
simpleObject.job = "Engineer"; // No effect in non-strict mode
console.log(simpleObject); // Output: { name: "Alice", age: 25 }

// Trying to delete a property
delete simpleObject.age; // No effect in non-strict mode
console.log(simpleObject); // Output: { name: "Alice", age: 25 }

In non-strict mode, the changes are silently ignored. However, if you run the same code in strict mode, it will throw an error.

"use strict";

const simpleObject = { name: "Alice", age: 25 };
Object.freeze(simpleObject);

// Trying to change an existing property
simpleObject.name = "Bob"; // Throws TypeError in strict mode
console.log(simpleObject); // Output: { name: "Alice", age: 25 }

// Trying to add a new property
simpleObject.job = "Engineer"; // Throws TypeError in strict mode
console.log(simpleObject); // Output: { name: "Alice", age: 25 }

// Trying to delete a property
delete simpleObject.age; // Throws TypeError in strict mode
console.log(simpleObject); // Output: { name: "Alice", age: 25 }
Example 2: Freezing a Nested Object

Freezing an object does not freeze its nested objects by default. You need to manually freeze nested objects.

const nestedObject = {
    name: "Bob",
    details: {
        age: 28,
        job: "Teacher"
    }
};

Object.freeze(nestedObject);

// Modifying the nested object is possible
nestedObject.details.age = 29; // This will succeed
console.log(nestedObject); // Output: { name: "Bob", details: { age: 29, job: "Teacher" } }

As shown, even though nestedObject is frozen, its nested details object can still be modified. To truly make an object immutable, you need to deeply freeze it.

Example 3: Limitations of Object.freeze()

Object.freeze() only affects the top-level properties of an object. Nested objects are not frozen.

const nestedObject = {
    name: "Charlie",
    details: {
        age: 30,
        job: "Doctor"
    }
};

Object.freeze(nestedObject);

// Nested object is not frozen
nestedObject.details.age = 31; // This will succeed
console.log(nestedObject); // Output: { name: "Charlie", details: { age: 31, job: "Doctor" } }

Common Use Cases

  • State Management: In applications, especially those built with React, immutable state is beneficial as it makes state changes predictable and easier to debug.
  • Threading and Concurrency: Immutability can simplify handling data in environments where multiple threads might access and modify data simultaneously.

Introducing Object.seal()

What is Object.seal()?

Object.seal() is another method in JavaScript that prevents the addition of new properties and marks all existing properties as non-configurable. However, it does allow the modification of existing properties.

Syntax

The syntax for Object.seal() is:

const object = { key: value };
Object.seal(object);

How Does it Work?

When you seal an object using Object.seal(), the object becomes less mutable in the following ways:

  • Properties Cannot be Added: New properties cannot be added to the object.
  • Properties Cannot be Deleted: Existing properties cannot be deleted from the object.
  • Existing Properties Can be Modified: The values of existing properties can be changed.

Examples

Example 1: Sealing a Simple Object

Let’s seal a simple object and try to modify it.

const simpleObject = { name: "David", age: 35 };
Object.seal(simpleObject);

// Trying to add a new property
simpleObject.job = "Artist"; // No effect
console.log(simpleObject); // Output: { name: "David", age: 35 }

// Trying to delete a property
delete simpleObject.age; // No effect
console.log(simpleObject); // Output: { name: "David", age: 35 }

// Trying to modify an existing property
simpleObject.age = 36;
console.log(simpleObject); // Output: { name: "David", age: 36 }
Example 2: Sealing an Object with Configurable Properties

When an object is sealed, its properties become non-configurable, meaning you cannot change their descriptors, such as making them non-writable.

const sealedObject = { name: "Eve", age: 22 };
Object.seal(sealedObject);

// Trying to add a new property
sealedObject.job = "Writer"; // No effect
console.log(sealedObject); // Output: { name: "Eve", age: 22 }

// Trying to delete a property
delete sealedObject.age; // No effect
console.log(sealedObject); // Output: { name: "Eve", age: 22 }

// Trying to modify an existing property
sealedObject.age = 23;
console.log(sealedObject); // Output: { name: "Eve", age: 23 }

// Trying to change the descriptor of an existing property
Object.defineProperty(sealedObject, 'age', { configurable: false }); // Throws TypeError in strict mode
console.log(sealedObject); // Output: { name: "Eve", age: 23 }
Example 3: Limitations of Object.seal()

Object.seal() prevents adding and deleting properties and marks existing properties as non-configurable. However, it does not make the object deeply immutable.

const nestedObject = {
    name: "Frank",
    details: {
        age: 40,
        job: "Chef"
    }
};

Object.seal(nestedObject);

// Nested object is not sealed
nestedObject.details.age = 41; // This will succeed
console.log(nestedObject); // Output: { name: "Frank", details: { age: 41, job: "Chef" } }

Common Use Cases

  • Configuration Objects: When you need to prevent accidental modifications to configuration objects.
  • API Responses: Freezing the response from an API to prevent mutation can help maintain data integrity.

Comparing Object.freeze() and Object.seal()

Key Differences

Object.freeze()

  • Properties Cannot be Changed or Added: After freezing, no properties can be added or modified.
  • Properties Cannot be Deleted: Existing properties cannot be deleted.
  • Deep Immutability: Nested objects are not automatically frozen, so you need to deep freeze them manually if needed.
  • Use Case: Ensuring that an object and all its properties remain unchanged.

Object.seal()

  • Properties Cannot be Added or Deleted: New properties cannot be added, and existing properties cannot be deleted.
  • Properties Can be Modified: The values of existing properties can be changed.
  • Deep Immutability: Nested objects are not automatically sealed, so you need to seal them manually if needed.
  • Use Case: Allowing modifications to property values but preventing modifications to the shape of the object.

Similarities

Both Methods

  • Object Shape is Protected: Both methods prevent adding or deleting properties, ensuring the shape of the object remains the same.
  • Non-extensible: Both methods make the object non-extensible, meaning no new properties can be added.

When to Use Which Method?

  • Use Object.freeze(): When you want to guarantee that neither the shape of the object nor its properties change.
  • Use Object.seal(): When you want to prevent structural changes but allow modifications to property values.

Practical Immutability in JavaScript

Real-World Use Cases

  • React State Management: React’s state management benefits from immutability because it allows React to efficiently detect changes and update the UI accordingly.
  • Data Integrity: Ensuring data integrity in applications where data should remain unchanged after a certain point.

Benefits and Drawbacks

Benefits:

  • Predictability: Improves code predictability by ensuring that objects remain unchanged.
  • Thread Safety: Simplifies handling data in multi-threaded applications.
  • Efficient Change Detection: Helps in efficiently detecting changes when used in state management libraries.

Drawbacks:

  • Complexity: Implementing immutability can add complexity, especially when dealing with deeply nested objects.
  • Performance Overhead: Freezing or sealing objects can introduce a performance cost.

Advanced Topics

Deep Freezing Objects

Understanding Deep Freezing

Deep freezing involves recursively freezing an object and all of its nested properties, making it truly immutable. This is important when you have complex, deeply nested objects.

Implementing Deep Freeze

To create a deep freeze function, you can use recursion to freeze each nested object.

Example of a Deep Freeze Function
function deepFreeze(obj) {
    // Retrieve the names of object properties
    const propNames = Object.getOwnPropertyNames(obj);

    // Freeze properties before freezing self
    for (let name of propNames) {
        let value = obj[name];

        if (value && typeof value === "object") {
            deepFreeze(value);
        }
    }

    // Freeze the object itself
    return Object.freeze(obj);
}

const nestedObject = {
    name: "George",
    details: {
        age: 50,
        job: "Photographer"
    }
};

deepFreeze(nestedObject);

// Trying to modify the nested object fails
nestedObject.details.age = 51; // No effect in non-strict mode
console.log(nestedObject); // Output: { name: "George", details: { age: 50, job: "Photographer" } }

In this example, the deepFreeze function recursively freezes each property that is an object, ensuring that the entire nested structure is immutable.

Performance Considerations

Freezing and sealing objects can introduce performance overhead because it involves traversing and modifying property descriptors. In most cases, the performance impact is negligible, but it’s important to consider it, especially in high-performance scenarios or when working with large objects.

Summary

Recap of Key Points

  • Immutability: Ensuring that objects cannot be changed after creation.
  • Object.freeze(): Freezes an object, making it immutable. Nested objects are not frozen by default.
  • Object.seal(): Seals an object, preventing addition or deletion of properties but allowing modification of existing properties.
  • Deep Freezing: Manually freezing all nested objects to ensure deep immutability.
  • Benefits: Improves predictability, simplifies debugging, and enhances performance in state management libraries.

Further Reading and Resources

Exercises

Practice Problems

Problem 1: Create and Freeze an Object

Create an object and freeze it. Attempt to modify its properties, add a new property, and delete a property to observe the results.

const fruit = { name: "Apple", color: "Red" };
Object.freeze(fruit);

// Now try to change, add, and delete properties
fruit.name = "Banana"; // This will not change the name
fruit.taste = "Sweet"; // This will not add a new property
delete fruit.color; // This will not delete the color

console.log(fruit); // Output: { name: "Apple", color: "Red" }

Problem 2: Create and Seal an Object

Create an object and seal it. Try to modify its properties, add a new property, and delete a property to observe the results.

const car = { make: "Toyota", model: "Corolla", year: 2020 };
Object.seal(car);

// Now try to change, add, and delete properties
car.year = 2021; // This will change the year
car.color = "Blue"; // This will not add a new property
delete car.model; // This will not delete the model

console.log(car); // Output: { make: "Toyota", model: "Corolla", year: 2021 }

Problem 3: Apply Deep Freeze to a Nested Object

Create a nested object and use a deep freeze function to make it immutable. Verify that the entire structure is immutable.

const person = {
    name: "Hannah",
    details: {
        age: 29,
        job: "Engineer"
    }
};

function deepFreeze(obj) {
    const propNames = Object.getOwnPropertyNames(obj);

    for (let name of propNames) {
        let value = obj[name];

        if (value && typeof value === "object") {
            deepFreeze(value);
        }
    }

    return Object.freeze(obj);
}

deepFreeze(person);

// Trying to modify the nested object fails
person.details.age = 30; // No effect in non-strict mode
console.log(person); // Output: { name: "Hannah", details: { age: 29, job: "Engineer" } }

Additional Reading

Further Resources

By understanding the concepts of Object.freeze() and Object.seal(), you can write more robust and predictable JavaScript code. These methods are particularly useful in modern JavaScript applications, especially when dealing with state management and data integrity.