This post will discuss the difference between var
, let
, and const
in terms of scope and hoisting in JavaScript. Once you understand the pros and cons of each, it makes it easier to interpret errors and avoid bugs.
Scope is the current context of execution. The context in which values and expressions are “visible,” or can be referenced. If a variable or other expression is not “in the current scope,” then it is unavailable for use.
Function Scope
If var
is defined inside of a function, it is function scoped. var
is globally scoped when defined outside of the function. If a variable defined with var
is defined inside of a function and called outside of that function, it will return a ReferenceError
.
The example below shows that when scrapedIce
is called, you can log out raspa
, which is defined inside of that function. If you console.log(raspa)
outside of the function, you will see global snow cone variable
because you have access to the global var snowcone
defined outside of the function. Finally, you will receive a ReferenceError
if you try to access the syrup
variable outside of the scrapedIce()
function.
var raspa = "the global snow cone variable";
function scrapedIce(){
var raspa = "the local snow cone variable";
var syrup = "sweet and tasty"
console.log(raspa)
}
scrapedIce(); // expected output: a local snowcone variable
console.log(raspa) // expected output: a global snowcone variable
syrup // expected output: Uncaught reference error: syrup is not defined
The variable defined in the local scope shadows the variable in the global scope. It is created and destroyed every time the function is executed, and it cannot be accessed by any code outside the function.
var
determines the variable’s scope, so what happens when var
is removed?
raspa = "the global snowcone variable";
function scrapedIce(){
raspa = "the local snowcone variable";
syrup = "sweet and tasty";
console.log(raspa)
}
scrapedIce(); // expected output: a local snowcone variable
console.log(raspa) // expected output: a local snowcone variable
syrup // expected output: "sweet and tasty"
You can access syrup
outside of the function because it’s not bound to that scope, since it’s a global variable. This is almost always a bad idea, and in general you should never use global variables in JavaScript, unless you really intend for something to be accessible (and mutatable) globally.
Block Scope
Variable definitions prefixed with let
and const
will be block/lexically scoped (a block is the code enclosed in curly brackets {}
), in contrast to variables defined with var
, which are functionally scoped. This property is handy because it limits variable scope to the block it is used in, which can prevent errors if you’re editing a big function where there could accidentally be variables with the same name (in general, this should be avoided, but we’ll save that for a later post).
let raspa = "snow cone outside of the block";
{
let raspa = "snow cone inside of the block";
}
console.log(raspa); // logs "snow cone outside of the block;
And we can see we get the same output if we use const
.
const raspa = "snow cone outside of the block";
{
const raspa = "snow cone inside of the block";
}
console.log(raspa); // logs "snow cone outside of the block;
Okay, let that sink in while you enjoy a refreshing raspa.
Hoisting: var, let, const
In JavaScript, variable and function declarations are stored into memory during the compile phase before any code is actually executed. This is referred to as hoisting because it seems like the variables and function delarations are magically “hoisted” to the top of the scope.
console.log(snowball);
var snowball = "what snow cones are called in New Orleans"
This is in contrast to logging variables that have not been defined at all:
console.log(foobarbaz) // Uncaught ReferenceError: foobarbaz is not defined
Because the compiler sees that somewhere in the scope, the snowball
variable is being defined, it moves the definition to the top of the scope, preventing an error from being generated. This can cause subtle bugs in your applications if you’re using var
, which initializes the variable when hoisting to undefined
:
var price = 2;
if (price > 3) {
var consumerOpinion = "Snow balls are $5 - that's too expensive!";
}
console.log(consumerOpinion)
// undefined
Since consumerOpinion
is hoisted to the top of the scope, the application continues executing instead of stopping due to a ReferenceError
, which would be preferable, since the branch that actually sets the variable to a sensible value can never be executed as-defined. For this reason, it makes to use let
to define your variable because it is not initialized to undefined
like var
, so it will generate a ReferenceError
:
let price = 2;
if (price > 3) {
let consumerOpinion = "Snow balls are $5 - that's too expensive!";
}
console.log(consumerOpinion)
// Uncaught ReferenceError: consumerOpinion is not defined
This generates an error, as expected, because the branch that sets the value of consumerOpinion
was never visited. Instead of silently continuing execution, the program errors out, sending an immediate signal to the developer that something went wrong which makes this issue much easier to investigate.
Const
As we learned earlier, const
and let
are block scoped. Unlike let
, const
cannot be reassigned. The examples below try to redeclare the snowball variable but to no avail.
const snowball = 'snowball';
snowball = 'raspa'; // Uncaught TypeError: Assignment to constant variable.
The next one doesn’t work either.
const snowball = "snowball";
const snowball = "raspa"; //Uncaught SyntaxError: Identifier 'snowball' has already been declared
Variables defined by const
are also hoisted but they are not initialized, like let
. In general you should use const
to accomplish your work whenever possible.
BONUS: Temporal Dead Zone
While it may appear that const
and let
declarations are not hoisted to the top of the scope like var
is, that is not the case. The only difference is that const
and let
do not assign undefined
to the freshly-hoisted variable declaration, which gives them the favorable property of generating ReferenceError
s when accessed unexpectedly. So what does it mean to be hoisted but unititialized?
const snowballStand = function() {
const snowballMaker = function() {
console.log(snowballType)
}
const snowballType = "Raspberry Raspa"
snowballMaker()
// log output: "Raspberry Raspa"
}
You might expect this code to generate a ReferenceError
since snowballType
hasn’t been set when it appears in the snowballMaker
function. However, that’s not how JavaScript works. Because const
and let
are both hoisted to the top of their scope (block scope), when the snowballType
logging code is evaluated, no error is generated. Also, an error is not generated when calling the snowballMaker
function, because in the line above we actually assign a value to this variable. If we tried to execute the function before we assigned a value to snowballType
, we would receive a ReferenceError
as expected. The time between the hoisted definition and the assignment to that definition is known as the temporal dead zone
. You can learn more about why it exists here
If you are interested in an even deeper dive on this topic, these articles might be useful: Dev.to, codeburst