Constants are mutable?!

I really like Javascript. I like reading about it, writing about it, and most of all, coding in it. I am a coding bootcamp graduate and until recently, I have only ever worked with client-side Javascript, doing all of my server-side coding in Ruby. I wanted to extend my knowledge of JS so I purchased a great Node.js course on Udemy and have been working my way towards building full-stack JS applications.

Something I have noticed from questions I get about my blog posts and even from the instruction in this Node course is that a ton of people are still hazy on the differences between let , var , and const . So I wanted to do a short post about the differences between them and my personal strategy for choosing which to use.

Const and block-scoping

This seems to be the one that I get the most questions about. A lot of people think that const has global scope no matter where it is declared. This is not true! Just like let , const is block-scoped.

What does this mean? This means that variables declared with const are only available in the scope of the block in which they are declared. I know that definition sucks, but think of it like this:

If you declare a const inside of a function or if-statement, you can declare a const with the same name inside another block without getting an error. Think of a block as the code chunks between curly-braces (the exceptions being objects and maps). This will work:

function someFunc() {

const hello = "hello";

return hello;

} if (1 === 1) {

const hello = "hello again";

} else {

const hello = "hi there";

}

Each of these hello variables are locally-scoped to the block in which they are declared. So the only places they are available are between the curly braces that surround them. You will not be able to access them in the global scope. So this will NOT work:

function someFunc() {

const hello = "hello";

return hello;

} if (1 === 1) {

const hello = "hello again";

} else {

const hello = "hi there";

} console.log(hello); // ReferenceError: hello is not defined

The console.log breaks, because there is no hello variable in that scope.

You cannot, however redeclare a const in the same block in which it is declared. So this will break:

const hello = "hello";

const hello = "hi there!"; // SyntaxError: Identifier 'hello' has already been declared

Now remember, the typical rules of scope still apply and child scopes will still inherit the scope of their parent. So this will also work:

function parentFunc() {

const hello = "hello, how are you";



return function(name) {

return `${hello} `${name}?`;

}

} parentFunc()("Mark"); // => "hello, how are you Mark?"

As will this:

function someFunc(name) {

const hello = "hello"; if (name === "Mark") {

return `hey ${name}.`;

} else {

return `${hello} ${name}!`;

}

} someFunc("Jamie"); // => "hello Jamie!"

In both cases, hello is scoped within the block it is declared and therefore available within any scopes that are children of that block.

But wait! This part is super important. Block-scoped variables can be redeclared in child scopes without breaking. This may seem counter intuitive, but this is perfectly valid code:

function someFunc() {

const hello = "hello"; if (true) {

const hello = "hi!";

console.log(hello);

}

console.log(hello);

} // "hi!"

// "hello"

When a const is declared in a child scope, JS looks for a variable with the same name in that block first. If it doesn’t find one, the variable is created in that block and for that scope.

When the const is referenced, the JS engine will look in the current scope, then to the parent, and so-on up the scope chain until it finds what it is looking for. So if it finds one in the current scope, it stops looking. That’s why this does not break.

So that’s block scoping! Both const and let are block-scoped, which is important to remember for later. But first, I want to go over what makes const unique.

Const and reassignment

What makes const different from let is that it cannot be reassigned. A const must be defined when it is declared, unlike let or var . So const must always be declared like this:

const hello = "hello";

Not like this:

const hello;

hello = "hello"; // SyntaxError: Missing initializer in const declaration

What this also means is that a variable declared with const must always point to the same place in memory.

Note that this does not mean that the data it points to is immutable! It is super mutable. Piscean even. You can manipulate an object all day and it will still be the same object. So this will work:

const person = { name: "Mark" };

person.age = 27;

person.favoriteColor = "green";

This is also fine:

const people = ["Mark", "Naomi", "Carol", "Jordan"];

people.pop();

people.push("Tristan");

But this is not fine:

const counter = 0;

counter++;

Because it is the same as this:

const counter = 0;

counter = counter + 1;

…which is reassigning the variable. You are not mutating 0 here, because 0 is always 0. You are telling counter to point to the integer 1, AKA reassigning it. Numbers are not mutable data!

Let

We said earlier that let is also block-scoped. So all the same rules about scoping for const apply to let . What makes them different is that let can be reassigned.

So this works:

let hello; if (true) {

hello = "hello";

} else {

hello = "goodbye";

} console.log(hello); // "hello"

As does this:

let counter = 0;

counter++; console.log(counter); // 1

It’s also a great choice for for-loop iterators:

const array = [1, 2, 3, 4, 5]; for (let i = 0; i < array.length; i++) {

console.log(array[i]);

} // 1

// 2

// 3

// 4

// 5

They cannot, however, be re-declared! This will break:

let hello = "hello";

let hello = "hi there!"; // SyntaxError: Identifier 'hello' has already been declared

But, since they are block-scoped like const , this will still work:

function someFunc() {

let hello = "hello"; if (true) {

let hello = "hi!";

console.log(hello);

}

console.log(hello);

} // "hi!"

// "hello"

Var and function scoping

The biggest difference between let and var is the way they are scoped. So we know let and const are scoped by block, whether that block be part of a function or a statement.

var on the other hand, is function-scoped. This means that variables declared with var are scoped according to their current execution context. Variables that are declared outside of a function are globally-scoped.

Take a look here:

var hello = "hello"; if (true) {

var hello = "goodbye";

} console.log(hello); // "goodbye";

Now look at what happens when it is declared inside a function:

var hello = "hello"; function someFunc() {

var hello = "goodbye";

console.log(hello);

} someFunc();

console.log(hello); // "goodbye";

// "hello";

The same rules about scope inheritance and the scope chain apply here, but only in the context of functions. Variables declared with var inside if-else statements or as iterators in for-loops are hoisted to the global scope (or, if that statement is inside a function, they are hoisted to the scope of the function) and available throughout the rest of the application.

So if you use var in a for loop, you get some weird stuff:

var array = [1, 2, 3, 4, 5];

var i = 0; for (var i = 0; i < array.length; i++) {

console.log(array[i]);

} console.log(i); // 1

// 2

// 3

// 4

// 5

// 5

On the last line, console.log(i) , the value of i is 5, because the i in the for-loop is scoped globally.

Let’s see what happens when we do the same for-loop using let:

let array = [1, 2, 3, 4, 5];

let i = 0; for (let i = 0; i < array.length; i++) {

console.log(array[i]);

} console.log(i); // 1

// 2

// 3

// 4

// 5

// 0

There we go, now our for-loop doesn’t have any side-effects.

Lastly, var can be redeclared. So this will not break:

var hello = "hello";

var hello = "goodbye";

This is not ideal, as you probably don’t want to redeclare variables. If you need a new variable, you should use a new variable name to avoid weird side-effects and the like. Of course you would never redeclare a variable on the next line as I have above, but you (or a coworker working on the same repo) might create a variable with the same name in a block somewhere 100 lines down and not notice.

Note: you can avoid this with “use strict” and many people did that for a long time. But now we have let and const to fix this problem.

Opinion time

So you may be thinking, “okay how do I know which one to use?” Well, here is my strategy. Not to say it is the only right way to go, but it has worked for me so far.

I never use var . I have never needed to. There might be some case where you need your variables hoisted out of your blocks, but I imagine that if that is the case, you can just refactor your code. The problem with var is that it can easily produce unintended side-effects. If you use var in a block and maybe your coworker doesn’t realize it, they might try to use the same variable name somewhere else and the result will be unexpected behavior.

Instead, I always declare my variables with const unless I know they need to be reassigned (like counters or iterators). If that is the case, I use let and know that it will only be available in the scope of that block and there will be no global side-effects. This can save you a ton of debugging confusion down the road and helps your coworkers read your code.