All 4 JavaScript Scopes Explained
October 10, 2022
If you have written even a single line of JavaScript code then you have used one of the four scopes of JavaScript without even realizing it. These different scope levels determine how your code will run, how easy your code is to write/change, and many other factors about your code so knowing all the nuances of each different scope is crucial. In this short article I will teach you what each scope level is, how they interact with your code, and what you can do with this knowledge to write better cleaner code.
If you prefer to learn visually, check out the video version of this article.
What Is Scope?
The first question we need to tackle is what scope even is. In JavaScript, and pretty much every other programming language, your code runs within some set scope. This scope determines what variables your code has access to, how new variables will interact with the rest of your code, and a few other things. The best way to think of scope is as a partition to help you separate different parts of your code from one another.
const outer = "Out"
function test() {
const inner = "In"
console.log(outer, inner)
// Prints: "Out", "In"
}
test()
console.log(outer, inner)
// Throws Uncaught Reference Error: inner is not defined
In the above example we have some simple code that defines a variable outside a function and inside a function. We then log both variables to the console from inside and outside the function. This works fine inside the function, but outside the function our log throws an error since we do not have access to the inner variable outside the scope for the function.
This example utilizes two of the four scopes in JavaScript to separate our code in a way that only certain parts of the code have access to certain variables. This is the main reason for different scope levels to exist, so let’s cover those different scopes now.
Scope Levels
The four different scope levels are:
- Global Scope
- Module Scope
- Block Scope
- Function Scope
This may seem like a lot to keep track of but in reality you will probably use module and block scope for 95% of all the code you write, so it is a bit easier to keep track of. This doesn’t mean you should ignore the other scopes, though, as it is important to understand how they work.
Global Scope
To get started we will talk about the simplest scope to understand which is the scope most developers use for all their code when getting started as it kind of just ignores all the restrictions the other scopes impose. To understand global scope imagine I have the following HTML/JS.
<script src="script.js"></script>
// script.js
const a = 1
console.log(a)
// Prints: 1
This simple example is using global scope to define its variables. The way global scope works is essentially any time you define a a variable at the top level of a file (outside any function/curly braces) it is considered global scope and can be accessed ANYWHERE in your entire application. This global access makes writing code easier at first since you don’t have to worry about variables being blocked by different scopes, but as you start to write more complex code this quickly becomes difficult to manage. This problem is even worse when you have multiple files.
<script src="script-1.js"></script>
<script src="script-2.js"></script>
// script-1.js
const a = 1
// script-2.js
console.log(a)
// Prints: 1
As you can see in the above example we defined a variable in script-1.js
and we are able to use that variable in script-2.js
. This is because the variable from script-1.js
is defined globally and can be used ANYWHERE in your code. Because of this particular feature of global scope I try to never use global scope in any of my applications. It is incredibly difficult to keep track of global scope as your application grows which is why I much prefer module scope.
Module Scope
Module scope is very similar to global scope, but with one minor difference. Module scope variables are only available within the file you define them in. This means they cannot be used in other files which is ideal when trying to mentally keep track of everything. In order to enter module scope you need to use type="module"
on your script tags. This does much more than just change the scope so if you are unfamiliar with ES modules you should check out my full ES module guide.
<script src="script-1.js" type="module"></script>
<script src="script-2.js" type="module"></script>
// script-1.js
const a = 1
console.log(a)
// Prints: 1
// script-2.js
console.log(a)
// Throws Uncaught Reference Error: a is not defined
With this one change we removed the biggest problem with global scope of variables being available in every file while keeping the benefits of being able to use the variable anywhere in the file it is defined. Because of this module scope is one of the two scopes I use all the time.
Block Scope
The other major scope I use all the time is block scope. Block scope is one of the easier scopes to understand because it lines up with the curly brace {}
. Essentially, anytime you have code inside curly braces that is considered its own block scope. This means things like functions, if statements, for loops, etc. all create their own block scope.
function test() {
const funcVar = "Func"
if (true) {
const ifVar = "If"
console.log(funcVar, ifVar)
// Prints: "Func", "If"
}
console.log(funVar, ifVar)
// Throws Uncaught Reference Error: ifVar is not defined
}
This example has two separate block scopes being defined. Each block scope is defined as the code between the curly braces and each block has access to all the variables that are between the curly braces but NOT within another set of curly braces as well as all variables of its parent scope.
In our case the two blocks we have are the test
function and the if
statement. Our test
scope has access to all the variables within the test
function that are not nested in another set of curly braces which means it has access to the funcVar
variable. Our test
scope would also have access to any global/module scope level variables since those are parents of the test
scope.
Our second scope is the if
statement and that scope has access to the ifVar
as well as everything the test
scope has access to since the test
scope is the parent scope of the if
scope.
The important takeaway with block scope is anytime you have curly braces you create a new block scope that has access to all scopes it is inside of, but none of the scopes inside it. This inside/outside rule is actually true for all scopes. Any scope will have access to the scopes that it is inside, but it will have no access to the scopes inside it.
{
const a = 10
}
console.log(a)
// Throws Uncaught Reference Error: a is not defined
You can even add curly braces anywhere you want in your code to create a scope. This is not something I do often but it can be useful.
Function Scope
The final scope is function scope and this is something you hopefully never have to worry about as it relates to the var
keyword. Variables defined with the var
keyword are scoped at the function level instead of the block level which means they only care about the curly braces of a function.
function test() {
var funcVar = "Func"
if (true) {
var ifVar = "If"
console.log(funcVar, ifVar)
// Prints: "Func", "If"
}
console.log(funVar, ifVar)
// Prints: "Func", "If"
}
This code is exactly the same as the code from the block level example, but we are using var
instead of const
to define our variables. You will see in this example our code will work just fine and that is because the var
keyword ignores block level scope so even though ifVar
is defined within our if
block it doesn’t matter for function scope.
This is honestly pretty unintuitive and var
as a keyword in general is difficult to use which is why I recommend never using var
. If you want to learn more about why I don’t like var
and the differences between var
, let
, and const
check out my complete var vs let vs const guide.
Multiple Variables With The Same Name
One important thing to understand about this scoping is how it works when you have multiple variables with the same name.
function test() {
const a = "Func"
if (true) {
const a = "If"
console.log(a)
// Prints: "If"
}
console.log(a)
// Prints: "Func"
}
In this example we have the variable a
defined both inside our test
function and inside our if
block. When we log out a
in the if
block we get the value of a
from the if
scope while when we log a
outside the if
block we get the value of a
from the test
scope.
The main takeaway from this code is that when you have two variables with the same name that are in different scopes they are actually two entirely separate variables. They have nothing to do with one another, they never overwrite each other’s values and they work exactly the same as two variables with different names. The only difference is if they have the same name it is now impossible to access the value of the outer scope variable since accessing a
will always access the most inner scope variable available.
I generally try not to use the same name of variables when writing code, but in some instances it can make writing your code easier to understand so it is sometimes necessary.
Conclusion
While it may sound confusing to have four different scopes in JavaScript, it is actually a bit simpler than that since we really only care about two of the four scopes. Understanding how these scopes work is also crucial in writing good clean code.