Categories
JavaScript

Why Should We Be Careful When Using the const JavaScript Keyword?

Spread the love

Since ES6 was released, lots of new features have been added to the JavaScript. One of them is the const keyword for declaring constants. It lets is create constants that can’t be reassigned to something else.

However, we can still change it in other ways. In this article, we’ll look at the often-overlooked characteristics of const and how to deal with the pitfalls.

Characteristics of const

const is a block-scope keyword for declaring constants that’s available within the block that it’s defined in. It can’t be used before it’s declared. This means that there’s no hoisting.

They can’t be reassigned and can’t be declared.

For example, we can declare a constant as follows:

const foo = 1;

We can’t access it before declaration, so:

console.log(foo);  
const foo = 1;

will get us the error “Uncaught ReferenceError: Cannot access ‘foo’ before initialization”

The scope can be global or local. However, when it’s global, it’s not a property of the window object, so it can’t be accessed from other scripts within the app.

It’s a read-only reference to a value. This means that it’s important to note that the object may still be mutable. This is something that’s often overlooked when we declare constants. The properties of objects declared with const can still be changed, and for arrays, we can add and remove entries from them.

For example, we can write:

const foo = {  
  a: 1  
};  
foo.a = 1;  
console.log(foo.a);

Then we get 1 from the console.log .

We can also add properties to an existing object declared with const :

const foo = {  
  a: 1  
};  
foo.b = 1;  
console.log(foo.b);

Again, we get 1 from the console.log .

Another example would be array manipulation. We can still manipulate arrays declared with const , just like any other object:

const arr = [1, 2, 3];  
arr[1] = 5;  
console.log(arr)

The console.log will be us [1, 5, 3] .

We can also add entries to it by writing:

const arr = [1, 2, 3];  
arr[5] = 5;  
console.log(arr)

then we get:

[1, 2, 3, empty × 2, 5]

As we can see, objects declared with const are still mutable. Therefore, if we want to prevent accidentally changing objects declared with const , we have to make them immutable.

Making Objects Immutable

We can make objects immutable with the Object.freeze method. It takes one argument, which is the object that we want to freeze. Freezing means that rhe properties and values can’t be changed.

Property descriptors of each property also can’t be changed. This means the enumerability, configurability, and writability also can’t be changed, in addition to the value.

A property being configurable means that the properties descriptors above can be changed and the property can be deleted.

Enumerability means that we can loop through the property with the for...in loop.

Writability means that the property value can be assigned.

Object.freeze prevents all that from happening by setting everything except enumerable to false .

For example, we can use it as follows:

const obj = {  
  prop: 1  
};Object.freeze(obj);  
obj.prop = 3;

In strict mode, the code above will get us an error. Otherwise, obj will stay unchanged after the last line.

Note that Object.freeze only freeze the properties at the top level of an object, so if we run:

/* 'use strict'; */  
const obj = {  
  prop: 1,  
  foo: {  
    a: 2  
  }  
};  
Object.freeze(obj);  
obj.foo.a = 3;

We’ll get:

{  
  "prop": 1,  
  "foo": {  
    "a": 3  
  }  
}

As we can see, obj.foo.a still changed after we called Object.freeze on it.

Therefore, to make the whole object immutable, we have to recursively call Object.freeze on every level.

We can define a simple recursive function to do this:

const deepFreeze = (obj) => {  
  for (let prop of Object.keys(obj)) {  
    if (typeof obj[prop] === 'object') {  
      Object.freeze(obj[prop]);  
      deepFreeze(obj[prop]);  
    }  
  }  
}

Then when we call deepFreeze instead of Object.freeze as follows:

const obj = {  
  prop: 1,  
  foo: {  
    a: 2  
  }  
};  
deepFreeze(obj);  
obj.foo.a = 3;

We get that that obj stays unchanged:

{  
  "prop": 1,  
  "foo": {  
    "a": 2  
  }  
}

The deepFreeze function just loops through all the own properties of obj and then call Object.freeze on it, if the value of the property is an object, then it calls deepFreeze on deeper levels of the object.

Primitive values are always immutable including strings, so we don’t have to worry about them.

Another good thing about freezing an object with Object.freeze is that there’s no way to unfreeze it since the configurability of the properties is set to false , so no more changes can be made to the structure of the object.

To make an object mutable again, we’ve to make a copy of it and assign it to another object.

Now that we know that const doesn’t actually make everything constant, we’ve to be careful when we use const . We can’t assign new values to an existing constant. However, we can still change the property values of an existing object assigned to a constant.

Constants declared with const aren’t actually constant. To make it actually constant, or immutable, we’ve to call Object.freeze on each level of the object declared with const to make sure it’s actually constant.

This only applies to object-valued properties. Primitives are immutable by definition so we don’t have to worry about them.

Leave a Reply

Your email address will not be published. Required fields are marked *