DEV Community

Cover image for Object Immutability In JavaScript
Haseeb Khan
Haseeb Khan

Posted on

Object Immutability In JavaScript

There are three ways to achieve object immutability in JavaScript.

  • Object.preventExtension(obj);
  • Object.seal(obj);
  • Object.freeze(obj);

In order to understand how object immutability works in JavaScript you must first understand how properties of any object are described.

Property Descriptors

Objects in JavaScript have properties and as of ES5 all properties are described with property descriptors. You can get the descriptor of a property in any object by

Object.getOwnPropertyDescriptor(obj, 'property');

What this returns is an object that contains the property’s value and its characteristics, how it’s described. Here’s how you’d use it

const obj = {
    x: 'some value of x'
}

const descriptors = Object.getOwnPropertyDescriptor(obj, 'x');

console.log(descriptors);

/* Returns
Object { 
    value: 'some value of x', 
    writable: true, 
    enumerable: true, 
    configurable: true 
}*/

By default, the objects property descriptors i.e. writable, configurable and enumerable are all set to true. We’ll cover each one in detail in this section.

To change the descriptor of a property you use the Object.defineProperty(obj, prop, descriptor) method. The three arguments it takes are

  • obj: The object you want to define the property on
  • prop: The property name to be defined or modified
  • descriptor: The descriptor for the property being defined or modified.

One thing to note is that when using the defineProperty method to add properties to an object, the descriptors of the new property default to false. The reason why all descriptors were set to true by default in the above example is because we didn’t use the defineProperty method to define ‘x’. Consider the following example in contrast to the one above.

const obj1 = {};

Object.defineProperty(obj1, 'prop1', {
  value: 42,
});

const descriptors = Object.getOwnPropertyDescriptor(obj1, 'prop1');

console.log(descriptors);

/* Returns
Object { 
    value: 42, 
    writable: false, 
    enumerable: false, 
    configurable: false 
}*/

Writable

When the writable descriptor is set to true, you’re able to assign and reassign values to the property. Here’s an example

let myObject = {
    x: 'someValue'
};

myObject.x = 'some other value';
myObject.x = 52;

Everything works as expected. However, when the value of the writable descriptor is set to false you won’t be able to change the value.

Let’s see how that’ll work.

let myObject = { x: 52 }; // writable is true

Object.defineProperty(myObject, 'x', {
    writable: false
});

// The following will silently fail
myObject.x = 42;

When reassigning the value of the property, the reassignment will silently fail unless you have 'use strict' (strict mode) turned on. If strict mode is turned on, JavaScript will throw an error telling you that you that the property is read-only.

Error: "x" is read-only

Configurable

When this descriptor is set to true, you’re able to change the descriptors of the property. Here’s an example

let myObj = {
    x: 52
};

// By default the configurable descriptor on 'x' 
// is set to true so you can do the following i.e. you can configure it.

Object.defineProperty(myObj, x, {
    enumerable: false
});

However, when the configurable descriptor is set to false you won’t be able to change the property descriptors (other than writable and value, you won’t be able to configure the property). Not only that but you also won’t be able to delete the property from the object either. This is specifically useful to have when you want to prevent accidental deletion of a property by some other part of your code. Here’s how it would look

let myObj = { x: 32 };

Object.defineProperty(myObj, 'x', {
    configurable: false
});

// Other than the 'writable' and 'value' descriptors, you can't re-configure this property;

// You also can't delete the property
delete myObj.x;

After setting the configurable descriptor to false and you try to re-configure a property in strict mode it’ll throw the following error

'use strict';

const object1 = { x: 2 };

Object.defineProperty(object1, 'x', {
  configurable: false
});

Object.defineProperty(object1, 'x', {
  enumerable: false
});

// Error: can't redefine non-configurable property "x"

And when you try delete the property it’ll throw

'use strict';

const object1 = { x: 2 };

Object.defineProperty(object1, 'x', {
  configurable: false
});

delete object1.x;
console.log(object1.x);

// Error: property "x" is non-configurable and can't be deleted

Enumerable

When this descriptor is set to true, the property will be read when iterating the object keys. That means the key will appear in a for…in loop, in Object.keys(). Here’s an example

const obj = {
    x: 52 //enumerable is true
};

Object.defineProperty(obj, 'a’, {
    value: 1 // enumerable is false
});

Object.defineProperty(obj, 'b’, {
    value: 1, 
    enumerable: true // self explanatory
});

obj.c = 53; // When creating properties by setting them, the descriptors are defaulted to true.

for (key in obj) {
    console.log(key) // x, b, c
}

console.log(Object.keys(obj)); // [x, b, c]

You can also check if the property is enumerable by using the propertyIsEnumerable method on the object like this

obj.propertyIsEnumerable('x'); // true
obj.propertyIsEnumerable('a'); // false
obj.propertyIsEnumerable('b'); // true
obj.propertyIsEnumerable('c'); // true

Object Immutability

Finally! Now that we know how object properties work and how they’re described it’ll be super easy to understand how Object Immutability works. The three methods for achieving different levels of Object immutability are

  • Object.preventExtensions()
  • Object.seal()
  • Object.freeze()

These methods essentially make use of property descriptors to make the object immutable. Lets look at them one by one

Object.preventExtension()

Objects by default are extensible, meaning properties can be added to them. To check if an object is extensible you can use the isExtensible method like this

const someObject = {};
Object.isExtensible(someObject); // true

and this can be changed by using the preventExtensions method like this

const someObject = {};
Object.preventExtensions(someObject);
Object.isExtensible(someObject); // false

Now when you try and add a property to this object an error will be thrown

Object.defineProperty(someObject, 'nProp', { value: 52 });
//  Error: can't define property "nProp": Object is not extensible

This is very useful when you want the user of your object to be able to do what ever with it but not add more properties.

Note: One thing to note here is that Object.preventExtensions() only prevents properties to the object it self. You’ll still be able to add properties to the [[Prototype]] of the object. How the [[Get]] and [[Put]] algorithms work in JavaScript is a topic for a separate post.

Object.seal()

This method calls Object.preventExtensions() on the object and also sets the configurable descriptor of all properties to false. This ensures that the user of your object won’t be able to add any more properties or remove (delete) properties from the object. Here’s an example

const obj = {
  x: 42
};

Object.seal(obj);
obj.x = 52;

console.log(obj.x); // 52

delete obj.x; // cannot delete when sealed
console.log(obj.x); // 52

Object.defineProperty(obj, 'y', { value: 24 });
//  Error: can't define property "y": Object is not extensible

Object.freeze()

This does exactly what it sounds like. It freezes the object by using Object.preventExtensions, Object.seal and also makes the writable descriptor of all properties to false.

This is the highest form of immutability you can have. It prevents new properties from being added to it, existing properties from being removed, prevents changing the enumerability, configurability, or writability of existing properties, and prevents the values of existing properties from being changed. Also, it prevents from modifying the [[Prototype]] of the object as well.

'use strict';

const obj = { 
    x: 52 
};

Object.freeze(obj);

obj.x = 32;
// Error: "x" is read-only

obj.y = 52; 
// Error: can't define property "y": Object is not extensible

Object.defineProperty(obj, 'y', { value: 52 });
// Error: can't define property "y": Object is not extensible

Object.defineProperty(obj, 'x', { enumerable: false });
// Error: can't redefine non-configurable property "x"

Object.defineProperty(obj, 'x', { writable: true});
// Error: can't redefine non-configurable property "x"

Conclusion

Understanding object immutability becomes much easier once you know how the different property descriptors work and are changed with the different methods.

You should now have an understanding of the different property descriptors available, how to modify them, the side effects of modifying them and how they’re used in object immutability. You should now also know the three methods you can use to achieve different levels of object immutability and how they work behind the scenes.

Have you had to modify property descriptors before? How about make objects immutable? Let me know in the comments below about your experience with these things and any problems you encountered when using them.

Cheers!

Top comments (0)