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.