Object Immutability In JavaScript
Tags: javascript, webdev, tutorial
Published: 2020-09-29 13:01:21

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!