by Gordon Zhu, December 2024
In this post, you'll learn everything you need to know about const
, let
, and var
. Along the way, you'll explore key concepts about memory and scope that will help you build a robust mental model for how everything works. At the end, you'll learn how to choose the right type of variable for any situation.
I explain everything in a way that builds on itself. So you should read the post from top-to-bottom without skipping around.
const
Variables declared with const
(short for constant) cannot be reassigned. This makes const
the most restrictive type of variable.
With const
, primitives work as you'd expect:
const numberDeclaredWithConst = 10; // If I try to reassign, I get an error. // "Uncaught TypeError: Assignment to constant variable." numberDeclaredWithConst = 15;
With const
, objects work as you'd expect too, IF you understand how assignments work:
const o = {}; o = {}; // "Uncaught TypeError: Assignment to constant variable." o.name = 'Gordon'; // But this works fine. Why?
With o = {};
, we're attempting to reassign our constant o
(a big no-no). So we get an error, as we'd expect, since as we mentioned already, reassignments are not allowed.
But why is o.name = 'Gordon';
allowed? Well, let's slow down and think carefully. We have a constant o
, which cannot be reassigned, but with o.name = 'Gordon';
have nothing of the sort! Instead, we are assigning a value to the name
property of o
. Assigning a property of o
is not the same thing as assigning o
!
Yes! Conceptually, an object is just a collection of 0 or more values. That's what distinguishes objects from primitives. Objects can contain multiple values, primitives can't.
With this in mind, I think of an object as simply a bag of stuff. Yes, think of an actual bag, like a shopping bag or backpack.
So when you have something like const bag = {}
, the bag
constant can't be reassigned. BUT modifying what's inside of the bag (analogous to modifying object properties, changing items in an array, etc) is totally cool! To use the formal computer science terminology, objects are mutable.
Unlike mutable objects, primitives in JavaScript are immutable (the opposite of mutable), meaning we cannot change them at all.
Say that you have const myNumber = 5;
. We know that 5
can't change, because it's immutable. And so the only way to change myNumber
would be to reassign it to a different immutable number. But variables declared with const
can't be reassigned!
Now you have the full picture on why primitives work as you'd expect with const
.
let
Variables declared with let
can be reassigned. It behaves exactly like const
in all other respects.
Primitives work as you'd expect:
let letNumber = 10; // Reassignments are cool! letNumber = 15;
Objects work as you'd expect too.
// First I'm an object. let letObject = {}; // Now I'm an array :) letObject = [];
Tying this back to the "bags" we discussed earlier.
When you have const bag = {};
, you're saying that bag
can't be reassigned to anything else. But as we saw earlier, modifying the contents of bag
is fine.
In contrast, when you have let bag = {};
, you're saying that bag
could initially be the empty backpack sitting on your floor. But later on, bag
could change to something entirely different, like the shopping bag on your kitchen counter, or even a primitive.
const
and let
have block scopeA block is 0 or more statements surrounded by curly braces. Blocks can stand on their own, or they can be part of if-statements, while-loops, for-loops, or functions.
{ // This is a block with one statement. console.log("I'm part of a block"); } if (true) { // This is an empty block. } function emptyFunction() { // An empty block in a function. }
A variable created with let
or const
is accessible in the block where it was created, or any nested blocks. In the example below, I'll use const
, but let
works exactly same way:
{ const numberDeclaredWithinBlock = 10; { // This works because we're in a nested block console.log(numberDeclaredWithinBlock); // 10 } } // We'll get an error because we're OUTSIDE of the block. // Uncaught ReferenceError: numberDeclaredWithinBlock is not defined console.log(numberDeclaredWithinBlock);
If you declare a variable with const
or let
outside of any block, it will be accessible globally throughout your program.
var
Variables declared with var
are similar to let
, in that they can be reassigned. The crucial difference relates to scope. With let
, we had to pay attention to all types of blocks. However, with var
, we only have to pay attention to function blocks.
I'll modify what I said about let
before so that you can see the difference:
A variable created with let or const `var` is accessible in the block function where it was created, or any nested blocks functions.
function myFunction() { var myName = 'Gordon'; } // Uncaught ReferenceError: myName is not defined console.log(myName);
If you declare a variable with var
outside of any function, it will be accessible globally throughout your program.
const
let
In professional settings, there isn't a good reason to use var
, but its simplicity (fewer restrictions) can make it a useful teaching tool.
The reasoning here is similar to why most teachers prefer using untyped languages with beginners. The beginning stage is the most fragile period, so anything that can reduce mental overhead is extremely valuable. After students have the basics down, they can handle more complexity.
At the highest level, I really do think of objects as bags of primitives. But if you go down one level in the conceptual hierarchy, you'll quickly run into the idea of memory addresses.
How memory is stored is a core concept in all programming languages and a crucial prerequisite to working with algorithms and data structures. But it's tricky to internalize—so much so that many professional developers have a hazy understanding.
Memory addresses are beyond the scope of this article, but if you're interested, I go into exhaustive detail in my course, Programming Foundations.