Implementing an Immutable Data Structure in JavaScript
In JavaScript, objects and arrays are mutable by default, meaning their contents can be changed after creation. This can lead to unexpected side effects and bugs in complex applications. This challenge will guide you to create an immutable data structure that prevents such modifications, promoting predictable code and easier debugging.
Problem Description
Your task is to create a JavaScript function or class that takes an existing JavaScript object or array and returns a new, immutable representation of it. This immutable version should prevent any attempts to modify its properties, elements, or structure directly.
Key Requirements:
- Immutability: The returned data structure must be immutable. Any attempt to change its properties (for objects) or elements (for arrays) should either fail silently or throw an error, without altering the original data structure.
- Deep Immutability: Immutability should extend to nested objects and arrays within the structure. Modifying a nested property should also be prevented.
- Preservation of Original Data: The original data structure passed into your function/class must remain unchanged.
- Support for Objects and Arrays: Your solution should work for both plain JavaScript objects and arrays.
Expected Behavior:
- Reading properties/elements from the immutable structure should work as expected.
- Attempting to assign a new value to a property/element should not change the structure.
- Attempting to add new properties/elements should not change the structure.
- Attempting to delete properties/elements should not change the structure.
Edge Cases:
- Empty objects and arrays.
- Nested structures with mixed types (objects within arrays, arrays within objects, etc.).
nullorundefinedvalues within the structure.
Examples
Example 1:
const originalObj = {
name: "Alice",
age: 30,
address: {
street: "123 Main St",
city: "Anytown"
}
};
const immutableObj = createImmutable(originalObj);
// Attempting to modify
immutableObj.age = 31; // Should not change originalObj.age or immutableObj.age
immutableObj.address.city = "Othertown"; // Should not change originalObj.address.city or immutableObj.address.city
console.log(originalObj);
// Expected Output:
// {
// name: "Alice",
// age: 30,
// address: {
// street: "123 Main St",
// city: "Anytown"
// }
// }
console.log(immutableObj);
// Expected Output:
// {
// name: "Alice",
// age: 30,
// address: {
// street: "123 Main St",
// city: "Anytown"
// }
// }
Explanation:
The createImmutable function should return a version of originalObj that cannot be modified. All assignment attempts (like immutableObj.age = 31 and immutableObj.address.city = "Othertown") should have no effect, and the original object should remain untouched.
Example 2:
const originalArr = [1, 2, { a: 10, b: [5, 6] }];
const immutableArr = createImmutable(originalArr);
// Attempting to modify
immutableArr[1] = 100; // Should not change originalArr[1] or immutableArr[1]
immutableArr[2].a = 20; // Should not change originalArr[2].a or immutableArr[2].a
immutableArr[2].b.push(7); // Should not change originalArr[2].b or immutableArr[2].b
console.log(originalArr);
// Expected Output: [1, 2, { a: 10, b: [5, 6] }]
console.log(immutableArr);
// Expected Output: [1, 2, { a: 10, b: [5, 6] }]
Explanation:
Similar to the object example, createImmutable should make originalArr's representation immutable. Modifications to elements or nested properties should fail silently or throw errors, preserving the original array's state.
Example 3:
const complexObj = {
settings: {
theme: "dark",
notifications: [
{ type: "email", enabled: true },
{ type: "sms", enabled: false }
]
},
user: null
};
const immutableComplex = createImmutable(complexObj);
// Attempting to modify deeply nested properties
immutableComplex.settings.notifications[0].enabled = false;
console.log(complexObj);
// Expected Output:
// {
// settings: {
// theme: "dark",
// notifications: [
// { type: "email", enabled: true },
// { type: "sms", enabled: false }
// ]
// },
// user: null
// }
console.log(immutableComplex);
// Expected Output:
// {
// settings: {
// theme: "dark",
// notifications: [
// { type: "email", enabled: true },
// { type: "sms", enabled: false }
// ]
// },
// user: null
// }
Explanation:
This example tests deep immutability. Even deeply nested properties like enabled within the notifications array should be protected from modification.
Constraints
- The solution must be implemented in JavaScript.
- Your
createImmutablefunction/class should handle plain JavaScript objects and arrays. It does not need to handle specialized instances (e.g., Date objects, custom class instances) unless they are plain data objects/arrays. - Performance is a consideration, but correctness and deep immutability are prioritized. The solution should not introduce excessive overhead for simple read operations.
- The
createImmutablefunction should accept a single argument: the data structure to make immutable.
Notes
- Consider using JavaScript's built-in features like
Object.freeze()andObject.seal()as potential tools. However, remember thatObject.freeze()is shallow by default, so you'll need a way to achieve deep immutability. - Think about how to handle recursive structures (though for typical data structures, this is less common and might be an advanced consideration if time permits).
- Decide whether attempted modifications should throw an error or fail silently. For this challenge, failing silently is acceptable, but throwing an error might be more indicative of an issue.
- The goal is to create a representation of the data that is immutable, not to fundamentally alter the original JavaScript objects/arrays themselves.