Var vs Let vs Const
Nicolas Mesa
Posted on October 17, 2018
Cross-posted from my personal blog https://blog.nicolasmesa.co.
Hi there!
In this post, we’re going to talk about javascript
. We’ll start by looking at some examples of var
, let
and const
variable declarations and their properties. Then we’re going to go through my recommendations of when to use each one.
var
var
is the original way to do variable declaration in javascript. Here’s an example of how you can declare a variable using var
:
var myVariable = 10;
var is function-scoped
Variables defined using var
are function scoped. This means that the variable is available anywhere within its enclosing function. For example:
function functionScopedExample() {
var a = 10;
if (a < 20) {
var b = 15;
} else {
var c = 30;
}
console.log(a, b, c); // logs: 10 15 undefined
}
There are a few things happening in this example. First, a
is available everywhere in this function (like in most programming languages). Second, b
is also available everywhere even though we declared and assigned it inside an if
block. Third, c
is also available everywhere in the function (even though our execution path doesn’t go into the else
statement)! One thing to note here is that the value of c
is undefined
(since the code in the else
statement is not executed), but c
was still declared. To understand why this happened, we’ll need to learn about Variable Hoisting.
Here is another example of function-scoped variables:
function functionScopedExample2() {
var a = 10;
function innerFunction() {
var b = 20;
console.log(a); // logs: 10
}
innerFunction();
console.log(a); // logs: 10
console.log(b); // error: Uncaught ReferenceError: b is not defined
}
This example shows that a
is available in its enclosing function (functionScopedExample2
). This includes other functions defined within it (for example the innerFunction
function). b
, however, can only be referenced from its enclosing function (innerFunction
) and not from outside.
Function-scoped declarations can have weird effects:
function functionScopedExample3() {
var myNumberArray = [];
for (var i = 0; i < 10; i++) {
setTimeout(function() {
myNumberArray.push(i);
}, 100);
}
// wait until all timeouts have executed.
setTimeout(function() {
console.log(myNumberArray); // logs: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
}, 500);
}
The example above looks pretty weird. Most programmers would expect myNumberArray
to have numbers from 0 - 9. Instead the array contains the number 10
10 times! This happens because the variable i
is in scope for the whole function (functionScopedExample3
). By the time the functions in the setTimeout
execute, i
already has a value of 10
. One way to fix this issue is to create another function that receives i
as an argument and executes the setTimeout
:
function fixedFunctionScopedExample3() {
var myNumberArray = [];
// a new scope is created every time this function is called.
// As a result, newI is isolated.
function executeTimeout(newI) {
setTimeout(function() {
myNumberArray.push(newI);
}, 100);
}
for (var i = 0; i < 10; i++) {
executeTimeout(i);
}
// wait until all timeouts have executed.
setTimeout(function() {
console.log(myNumberArray); // logs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}, 500);
}
Later, we’ll see how to solve this problem by using let
instead of var
.
Variable hoisting
When we define a variable using var
, the variable declaration (not the assignment) is hoisted up to the beginning of the function. After the hoisting process, functionScopedExample
(defined above) would end up looking like this:
function varHoistingExample() {
// variables are hoisted to the top and all of them start
// with the value of undefined.
var a;
var b;
var c;
// a gets its value here.
a = 10;
if (a < 20) {
// b gets its value here.
b = 15;
} else {
// c never gets its value so it remains undefined.
c = 30;
}
console.log(a, b, c); // logs: 10 15 undefined
}
Hoisting can cause a lot of confusion. Take a look a the following example:
function weirdHoisting() {
color = 'yellow';
var color;
console.log('My favorite color is', color); // logs: My favorite color is yellow
}
Wait, what? How did that work? Remember, first the variables defined with var
are hoisted and then the function code is executed. In this example, the code after variable hoisting would look something like this:
function weirdHoisting() {
// variable declaration is hoisted to the top of its enclosing function.
var color;
color = 'yellow';
console.log('My favorite color is', color); // logs: My favorite color is yellow
}
Variable color
’s declaration is hoisted and thus prevents any reference errors.
Named function declarations are hoisted as well (there are some edge-cases if you’re doing conditionals). Here’s an example:
function functionHoisting() {
hoisted(); // logs: I am hoisted
notHoisted(); // error: Uncaught TypeError: notHoisted is not a function
var notHoisted = function() {
console.log('This will not work');
}
function hoisted() {
console.log('I am hoisted');
}
}
In this example, we see that the hoisted function can be called before it is declared. The notHoisted
variable, however, hasn’t been assigned the function
by the time it is called. Note that the function doesn’t fail because the notHoisted
name hasn’t been declared, but because it doesn’t point to a function (yet). After the hoisting process, this function would end up looking something like this:
function functionHoisting() {
// variable and function declaration are hoisted.
var notHoisted;
function hoisted() {
console.log('I am hoisted');
}
hoisted(); // logs: I am hoisted
notHoisted(); // error: Uncaught TypeError: notHoisted is not a function
// function would be assigned here but we never reach this.
notHoisted = function() {
console.log('This will not work');
}
}
let and const
let
is a newer way to define variables in javascript. Here’s an example of a variable definition using let
:
let myVariable = 10;
const
is a way to define constants in javascript. Here’s an example of a constant defined using const
:
const myConstant = 200;
let and const are block-scoped
let
and const
are both block-scoped. This means that the variable/constant is available anywhere in the block after it has been declared. let
/const
definitions are NOT hoisted. Let’s go through a few examples:
function blockScopedExample() {
let a = 10;
if (a < 20) {
let b = 15;
} else {
let c = 30;
}
console.log(a); // logs: 10
console.log(b); // error: Uncaught ReferenceError: b is not defined
// we would never reach this since the code fails in the line before
// but this would be the output if it had been executed
console.log(c); // error: Uncaught ReferenceError: c is not defined
}
This example shows that b
and c
are block-scoped. This is the reason why they’re not available outside of the if
/else
block. a
was defined at the beginning of the function, making it available anywhere inside the function block. Declaring something with let
at the beginning of a function is analogous to defining it with var
.
Remember the example where our array ended up with the same values? Well, this problem is easily solvable by using let
instead of var
to define i
:
function blockScopedExample2() {
let myNumberArray = [];
// let makes the variable i block-scoped
for (let i = 0; i < 10; i++) {
setTimeout(function() {
myNumberArray.push(i);
}, 100);
}
// wait until all timeouts have executed.
setTimeout(function() {
console.log(myNumberArray); // logs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}, 500);
}
Since the variable i
is block-scoped instead of function-scoped, its value remains the same for the whole block execution.
Here’s another example showing that variables defined with let
are not hoisted:
function noHoisting() {
color = 'yellow'; // error: Uncaught ReferenceError: color is not defined
let color;
// this is never reached.
console.log('My favorite color is', color);
}
The output of this function is what most programmers coming from other languages expect (having to declare a variable before using it).
So far, every example (except for blockScopedExample2
) we’ve covered using let
would have the same outcome if we had used const
instead. So, what does const
give us?
Constants
const
is how constants can be declared in javascript. After a constant has been defined using const
, the constant can’t have another value assigned. Here’s an example:
function cantReassignConstants() {
const myConstant = 10;
myConstant = 9; // error: Uncaught TypeError: Assignment to constant variable
}
Constants defined with const
also have to be assigned at declaration time (unlike let
and var
). For example, the following piece of code would fail during parsing (not at run-time).
function constantsCantBeDeclaredAndNotDefined() {
const myConstant;
myConstant = 5;
}
If you try to define this function, you would see an error like this:
Uncaught SyntaxError: Missing initializer in const declaration
Since this error occurs during parsing, the declaration of the function fails and we can’t even call it.
constantsCantBeDeclaredAndNotDefined(); // error: Uncaught ReferenceError: constantsCantBeDeclaredAndNotDefined is not defined
One thing to note about constants defined with const
is that only the assignment is constant. The object assigned to a constant doesn’t become immutable. For example, the following code runs with no errors:
function constDoesntMakeObjectsImmutable() {
const myObjectConstant = {
myKey: 'myValue'
};
myObjectConstant.myKey = 'something else';
console.log('myObjectConstant.myKey =', myObjectConstant.myKey); // logs: myObjectConstant.myKey = something else
}
Object.freeze()
If you want to make the object immutable, you can use Object.freeze()
before assigning the object:
function freezingObject() {
const myFrozenObject = Object.freeze({
myKey: 'This value cannot be changed'
});
myFrozenObject.myKey = 'something else';
console.log('myFrozenObject.myKey = ', myFrozenObject.myKey); // logs: myFrozenObject.myKey = This value cannot be changed
}
Even though the value of myKey
didn’t change, the assignment didn’t fail either (it failed silently). If you want it to fail, use the 'use strict;'
at the beginning of the function like this:
function freezingObject() {
'use strict';
const myFrozenObject = Object.freeze({
myKey: 'This value cannot be changed'
});
myFrozenObject.myKey = 'something else'; // error: Uncaught TypeError: Cannot assign to read only property 'myKey' of object '#<Object>'
// never executed
console.log('myFrozenObject.myKey = ', myFrozenObject.myKey);
}
Object.freeze
will only freeze the object
that we pass as an argument. If the object
contains references to other objects, those can still be modified unless we freeze them as well. For example:
function objectFreezeIsShallow() {
const myFrozenObject = Object.freeze({
key1: {
reassignable: 'reassign me'
},
key2: Object.freeze({
notReassignable: 'This value cannot change'
})
});
myFrozenObject.key1.reassignable = 'reassigned';
myFrozenObject.key2.notReassignable = 'this will not work';
console.log('myFrozenObject.key1.reassignable =', myFrozenObject.key1.reassignable); // logs: myFrozenObject.key1.reassignable = reassigned
console.log('myFrozenObject.key2.notReassignable =', myFrozenObject.key2.notReassignable); // logs: myFrozenObject.key2.notReassignable = This value cannot change
}
In this case, neither key1
or key2
can be reassigned since they’re frozen. The inner object of key1
however, is not frozen and the reassignable
key is set to a different value. Note that the notReassignable
key didn’t have its value reassigned since the object was frozen.
Recommendations
So, which one(s) should we use and why?
There are lots of reasons to use one over the other, so I divide this into a section per reason. Note that these are my preferences and it’s perfectly fine for other programmers to think differently :)
Never use the three of them in the same project
Decide which ones you want to use and stick to those. Using all three of them will be confusing. It will be difficult to know which one to pick and the code will be harder to understand. Be nice to the future readers of your code (including yourself).
Prefer consistency
If you’re working on an old project that is using var
, keep using var
. This will keep the project consistent.
Put variable declarations at the top if using var
If you’re using var
, place all variable declarations at the top of the function. This will help you see which variables are available in the function and there won’t be any hoisting surprises.
Consider older browser support
If your code needs to support older browsers that don’t have let
or const
, consider using var
instead. Alternatively, use a transpiler that converts all let
and const
declarations to var
.
Prefer let over var
If you’re starting a new project, I would encourage you to use let
over var
. let
is more predictable, especially for developers coming from a different language.
Prefer const over let when possible
I am a firm believer that everything should be a constant except for very special cases in loops or things like that (in those cases, fall-back to let
). I’ll probably write another post on why using const
(final
in Java) is a good choice, but the gist is:
Debugging
It will ease debugging. For instance, consider the following code:
function useConstWhenPossible(myOtherObject) {
const myObject = myOtherObject.getMyObject();
// ...
// 100 lines of code
// ...
myObject.doSomething(); // error: Uncaught TypeError: Cannot read property 'doSomething' of undefined
// ...
// 100 lines of code
// ...
return ...;
}
When you see the error, you go to the constant declaration. You can immediately tell that myOtherObject.getMyObject()
returned undefined
. There is no need to look through the other 100 lines of code to find if myObject
ever gets reassigned.
Variable naming
You will have to come up with good names for your constants because you won’t be able to reuse them. This will make your code easier to read and more self-documenting.
Helps prevent bugs
Another developer (or maybe even you) won’t have a chance to define an existing variable and break the whole function.
Use named functions
This will save you a lot of headaches, especially when moving code around. You won’t need to make sure that that your function calls are after the function declarations and everything will work consistently.
Posted on October 17, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.