Deciding between const, let, and var in JavaScript

description
Because ES6 is a foundational aspect of JS, I thought it would be fun to examine one of my favorite features that came with the release: let and const.
tags
javascript
published_on
May 6, 2022
As of 2022, JavaScript and its ecosystem continue to have a stranglehold in the world of web development. With tools like React and Next.js, it’s never been easier for JavaScript developers to sling code.
However, none of these frameworks would be nearly as effective without ES6 (ie. ECMAScript2015 for my JS nerds 🤓).
Because ES6 is a foundational aspect of JS, I thought it would be fun to examine one of my favorite features that came with the release: let and const.
When it comes to safety and predictability in JavaScript, these two little keywords have stepped up in a big way.
If you’re intrigued and want to learn a thing or two then let’s jump down a rabbit hole together!
💡 Hold up: Before we go any further, this post was inspired by Tyler McGinnis’s fantastic article. I learned a lot from him and ui.dev and wanted to pass on the knowledge in my own way.

Let’s Explore: Variable Declaration & Initialization

Before ES6, when using JavaScript, we would declare a variable using the var keyword.
var ninja;
In the code block above, we created a new variable named ninja. In JavaScript, variables are initialized with the default value of undefined.
var ninja; console.log(ninja); // undefined
Besides variable declaration, we can also use variable initialization when defining new identifiers.
Using this process means that we can initialize (ie. assign) the variable a value during its creation.
var ninjaOne; ninjaOne = "This is the first way to initialize me!"; var ninjaTwo = "This is the second and more common way to initialize me!";
In the above code, we’re declaring a variable and initializing it with a string value.
 

Recap: Declarations & Initializations

💡
Variable Declarations:
Variables in JavaScript can be declared with the var keyword. When no initial value is provided, these variables are assigned undefined by default.
Variable Initializations:
We can initialize a variable by assigning it a value during its creation
 
Now that we understand variable declaration and initialization, we can transition to the concept of Scope.

Let’s Explore: Scope

Scope defines the accessibility of variables inside your program. In JavaScript, there are two types of scope: global and functional.
Global scope: variables in this category are available throughout your program and can be accessed from anywhere.
var company = "Apple"; function getCompany() { return company; // Apple }
In the code block above, we declare a variable named company and initialize it with the string “Apple”. Because this variable was created outside of a function, we can consider it to be globally scoped.
In contrast to global scope, we can also have function scoped variables.
Function scope: variables in this category are only available from within the function they were created.
function getCompany() { // I can only be accessed from inside this function var company = "Apple"; console.log(company); // Apple }
Above we can see how to declare and use a local variable. These types of variables will exist for as long as their parent function is executing.
Note: We can also access these values from other functions that are nested within the parent function.
function getCompany() { var company = "Apple"; // I have access console.log(company); function extendCompanyName() { // I too have access return company + " Computers"; } extendCompanyName(); // Apple Computers }
However, attempting to access these local variables from outside the function will result in an error.
function getCompany() { var company = "Apple"; return company } getCompany(); // Apple // this does not work console.log(company); // ReferenceError: company is not defined
In the block above, we tried to access the company variable outside of the function it was declared in.
The error occurs because company is scoped to the getCompany function, and is only available from inside getCompany and any nested functions defined within it.
function getCompany() { var company = "Apple"; // I have access! 😎 console.log(company); function extendCompanyName() { // I too have access! 😎 return company + " Computers"; } extendCompanyName(); // Apple Computers } getCompany(); // I do not! 😒 console.log(company); // ReferenceError: company is not defined

Recap: Global vs Functional Scope

💡
Global Scope:
Variables can be accessed from anywhere in our program
Functional Scope:
Variables can only be accessed from within the function they were declared in. This includes nested functions!
 

Functional Scope vs Block Scope

Now that we’re warmed up, let’s take a look at a more advanced case regarding functional scope.
Imagine I’m an Apple retailer who wants to find out how many Macbook Pros I currently have in stock.
For the sake of this example, let’s also assume that I’m a big “Do-It-Yourself” kind of guy and decided to create an inventory system from scratch without having any development experience.
This has led to the unfortunate situation where my entire inventory is stored in two separate arrays of strings (the horror 🤯).
The first array contains a list of product names while the second array contains the price for each product. Luckily, the items found at each index position of the arrays are for the same product.
The current state of the inventory looks something like this
var names = ["Macbook Pro", "iPhone", "iPad", "Macbook Pro"]; var prices = ["2100", "500", "700", "2100"];
Because I’m someone who learns from their mistakes, I’ve to decided to hire you to fix the mess I’ve made. Your job will be to refactor my inventory into something that’s not completely insane.
One way to clean up this mess might be as follows
var names = ["Macbook Pro", "iPhone", "iPad", "Macbook Pro"]; var prices = ["2100", "500", "700", "2100"]; var inventory = []; function somethingLessInsane() { for (var n = 0; n < names.length; n++) { for (var p = 0; p < prices.length; p++) { if (n === p) { var name = names[n]; var price = prices[p]; // inventory items are now objects with a name and price var product = {price: price, name: name}; inventory.push(product); } } } }
Because the index for each array corresponds to a product, you decided to loop through each of the lists and initialize a new product object when the indexes matched. After creating the product you could then store it in a new array named inventory.
Although this script isn’t your best work, you decided it was best to complete your task as fast as possible and then get the heck away from me.
Now, what does this ridiculous example have to do with functional scope?
Well, it actually exposes a quick found in the JavaScript language. If we take a look at the variables within the nested for loop, you would think they can’t be accessed from outside right?
If you said yes, it turns out that you’re wrong. 🤷🏾‍♂️
var names = ["Macbook Pro", "iPhone", "iPad", "Macbook Pro"]; var prices = ["2100", "500", "700", "2100"]; var inventory = []; function somethingLessInsane() { for (var n = 0; n < names.length; n++) { for (var p = 0; p < prices.length; p++) { if (n === p) { var name = names[n]; var price = prices[p]; // inventory items are now objects with a name and price var product = {price: price, name: name}; inventory.push(product); } } } // during the first pass of the nested loop console.log(n); // 0 console.log(name); // Macbook Pro console.log(price); // 2100 }
If you came from a language like Java which uses blocked scope (every pair of {} is a block with its own scope), you would think that the code above results in error upon error. However, in the world of JavaScript, if we declare variables with var this is perfectly normal.
It’s important for the rest of this lesson to remember that using var within a function results in a functionally scoped variable. Which explains why the nested variables we saw above can be accessed by the parent function.

Recap: Scope

💡
Global Scope:
Variables can be accessed from anywhere in our program
Functional Scope:
Variables can only be accessed from within the function they were declared in. This includes nested functions!
Block Scope:
When a code block is defined (ie. with ‘{}’), all the variables declared within it are limited to that scopeIf statement and for-loop blocks now have a local scope!
 
Now that we understand variable declarations, initializations, and scope, we can move on to hoisting. This is the final concept we need to examine before finally jumping into why let and const are so important.

Hoisting

Hoisting is a process in JavaScript where variable and function declarations are moved to the top of their scope before any code is executed.
function somethingLessInsane() { // JavaScript creation phase sets function variables var n = undefined; var p= undefined; var name = undefined; var price = undefined; var product = undefined; for (n = 0; n < names.length; n++) { for (p = 0; p < prices.length; p++) { if (n === p) { name = names[n]; price = prices[p]; // inventory items are now objects with a name and price product = {price: price, name: name}; inventory.push(product); } } } // during the first pass of the nested loop console.log(n); // 0 console.log(name); // Macbook Pro console.log(price); // 2100 }
In the code above, JavaScript will grab all of the function’s variable declarations and initialize them with undefined during the creation phase.
This is why undefined is returned if we try to access these values before they were declared.
function getCompany() { // creation phase: var company = undefined; // access before declaration console.log(company); // "undefined". :( // location of actual declaration company = "Apple"; // access after declaration console.log(company); // Apple :) }
Note: Experienced JavaScript developers will know that Hoisting is the feature that allows us to call functions before they have actually been defined!
// JavaScript see's nothing wrong with this statement hoistMe(); // bar function hoistMe() { return "bar"; }
If the above code looks confusing that’s totally fine. Although the process is similar, hoisted functions behave in a slightly different manner.
One caveat to remember when functions are hoisted is that only function declarations can be hoisted successfully. This means that hoisting function expressions will not work.
Okay! Now that we’ve achieved mastery over var, we can finally get to the main point of this article: why did ES6 add const and let to JavaScript?

Const vs Let vs Var: a battle of scope

You’ll remember from our discussion earlier that there are two different types of scope in JavaScript: global and function.
Prior to ES2015 (ES6), this was definitely the case. However, ES6 introduced a third type of scope: block.
Remember when I discussed block scope during our examination of functional scope?
No?! Okay here’s a refresher.
Blocked scope: a variable with block scope is only available inside the block (remember the curly braces!) ****it was created in as well as any nested blocks. This means that if statements and for loops will now have their own scope.
So how does this relate to our discussion of var, let, and const? Well the main difference between them is that var is function scoped while let and const are blocked scoped.
Let’s see what this looks like in practice by jumping back to our Apple retailer example.
function somethingLessInsane() { for (var n = 0; n < names.length; n++) { for (var p = 0; p < prices.length; p++) { if (n === p) { var name = names[n]; var price = prices[p]; // inventory items are now objects with a name and price var product = {price: price, name: name}; inventory.push(product); } } } // during the first pass of the nested loop console.log(n); // 0 console.log(name); // Macbook Pro console.log(price); // 2100 }
Recall that with var we could access the name,, and price variables from outside their respective code blocks.
However, if we change the variable declarations from var to let we will get a different result.
function somethingLessInsane() { for (let n = 0; n < names.length; n++) { for (let p = 0; p < prices.length; p++) { if (n === p) { let name = names[n]; let price = prices[p]; // inventory items are now objects with a name and price let product = {price: price, name: name}; inventory.push(product); } } } // during the first pass of the nested loop console.log(n); // ReferenceError: n is not defined console.log(name); console.log(price); }
After running the code we encounter that pesky ReferenceError again! What this means is that we can no longer access the variables from outside their respective blocks.
This ES2015 update also impacts how hoisting works.
// before ES6 function getCompany() { // creation phase: var company = undefined; // access before declaration console.log(company); // "undefined". :( // location of actual declaration company = "Apple"; // access after declaration console.log(company); // Apple :) } // after ES6 function getCompany() { // access before declaration console.log(company); // ReferenceError: company is not defined // location of actual declaration let company = "Apple"; }
Switching from var to let now protects us from instances where we call a variable before it has been declared.
And really, I can’t think of any real-world examples where you would want to call a variable before it’s been declared. So really it's no big loss!

Let vs Const

Okay, so now that we know how the battle between var vs let plays out, what about const?
As it happens, const behaves just like let except for one key difference. When you use const to declare a variable, you’re telling JavaScript that its value should not be changed.
let company = "Apple"; const founder = "Steve Jobs"; company = "Grapes"; // this works! founder = "Steve Wozniak"; // this does not!
We can see from above that let allows the reassignment of variable values while const does not. So basically, any time we want to make a variable immutable we should use const.
Upon further inspection, const does seem suspiciously close to “constant” 🤔.

Recap: Var, Let, Const

💡
var:
Function scopedReturns undefined when accessing a variable before the declaration
let:
Block scopedReferenceError is thrown when accessing a variable before the declaration
const:
Block scopedReferenceError is thrown when accessing a variable before the declaration. Variables declared with const are considered immutable and can’t be changed.

Decision Time

Okay! Now that we have completely mastered JavaScript variable declarations 😉, you might be wondering which of these keywords you should be using.
Well in my not so humble opinion, you should be using const for the majority of your variable declarations. By defaulting to const, you let your team (and future self) know that the variable in question shouldn’t be changed. However, when working with variables that you know need to be modified (for loops I’m looking at you!) you should use let.
From now on these two keywords should be the only way you declare variables in JavaScript. var we had some good times, but the party's over and we want something more stable and predictable.
During this article, we explored the nature of variable declaration in JavaScript.
We learned how variable initialization, function scopes, and hoisting work; and the problems that can occur when using var. We also examined block scope and the issues const and let are meant to solve. Finally, we determined when to use const and let, and why var should be retired.
Thanks so much for jumping down this rabbit hole with me!
I really hope you were able to gain something valuable from this post.
If nothing else, just remember the following:
💡 const > let > var