The scope and scope chain are fundamental concepts in JavaScript and other programming languages. Also, one of the most confusing concepts at the beginning. Understanding the scope and scope chain is crucial for writing efficient, clean code and essential to building a solid foundation and mastering JavaScript.
If you are new to JavaScript, you might be struggling to understand these concepts. I remember how long it took me to get a firm understanding of these two tricky concepts. In this post, we will go through the scope and scope chain with some simple examples to clear out the confusion.
Without further ado, let's get started 😊
What is Scope?
Have you ever wondered why you can't access some of the variables outside a function? Or did you find it strange that you can have the same variable name outside a function and inside a function as well? The reason for this strange behavior is that every variable, function, or code block has its own scope.
According to MDN, the 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.
What does this mean?
Scope in JavaScript refers to the accessibility or visibility of variables and expressions. That means the space where an item, such as a variable or a function, is visible and accessible in your code.
For example, once a variable is declared, it can only be accessible within the scope that it has been declared in and will not be accessible outside the scope.
Let's look at a couple of examples to understand this.
const userName = "Sarah";
console.log(userName); // "Sarah"
In the above example, we have declared a variable userName
and assigned the value of Sarah
. No issue or error is coming up when we want to access this variable and print the name to the console.
Now let's declare this variable inside a function and print the value to the console outside the function.
function greeting() {
const userName = "Sarah";
console.log(`Hello ${userName}!`);
}
greeting(); // "Hello Sarah!"
console.log(userName); // ReferenceError: userName is not defined
In the above example, when trying to log the variable, JavaScript throws an error, ReferenceError: userName is not defined
. This is because the greeting
function creates a scope for the userName
variable. And the userName
variable can be accessed only within this scope, inside the function.
You might think that this behavior is strange. But having a scope for variables and expressions helps us to write efficient code and avoid conflicts and errors within our code.
Why is Scope important?
1. Ownership
One of the main benefits of scope is ownership. If we can access all variables from anywhere within our program, it will lead to unintended modifications to the variable from other parts of the program. Which means anyone can change them from anywhere at any given time.
With scoping, we can only access the variables in a certain area of the code. The scope helps to avoid these modifications, which help us to write secure code.
2. Avoid name collision
The scope helps to avoid name collision. For example, imagine that you have to use the same variable name in a different place in your program for a different purpose, or someone else from your team has already declared a variable in the global scope, and you want to identify the boundary of this variable.
Having clear scope on where you can access a variable makes it easier to identify its boundary, avoid assigning more values to the same variable, and use the same variable name in multiple locations within the code without altering the values.
3. Garbage Collection
In dynamic languages like JavaScript, when we complete the usage of a variable, the data will be automatically garbage collected. If we don't have a clear scope on where we can access certain variables, the compiler will not be able to identify when to collect the garbage, except at the end.
Having a clear scope on where variables can be accessed helps the compiler to garbage collect these variables at the end of each scope.
Types of Scope
JavaScript has three different types of scope.
- Global Scope
- Function Scope
- Block scope
Let's take a few examples to understand these three different scopes.
Global Scope
Variables declared outside of functions or code blocks (curly braces { }
) are considered to have a global scope. The outermost scope contains the entire code, and there is only one global scope in the program.
The variables defined in the global scope are named Global Variables and can be accessed and altered in any other scopes.
Check the below example. The function greeting
can access the userName
variable inside the function, and it is located in the global scope.
// Create a variable in the global scope
const userName = "Sarah";
function greeting() {
// Access global variable within the function
console.log(`Hello ${userName}!`);
}
greeting(); // "Hello Sarah!"
We have the accessibility to change the value of the variable anywhere in the code with global scope. Check the below example.
// Create a global variable
let userName = "Sarah";
function greeting() {
// Assigne a different value to the global variable
userName = "Jessica";
console.log(`Hello ${userName}!`);
}
greeting(); // "Hello Jessica!"
console.log(userName); // "Jessica"
In the above example, we have reassigned the value of the variable userName
inside the function. And it has modified the value of the variable inside the global scope.
This means that we can alter global variables anywhere within our code. Therefore, it is advised to only use global variables if and only if necessary as a best practice.
Let's move on to the function scope.
Function Scope
Each and every function creates its own scope. And the variables declared inside that function are only accessible inside that function and any of its nested functions. This is also called Local Scope.
Check the below examples to understand the function scope.
function calcAge(birthyear) {
// Declare variables inside the calcAge function scope
const currentYear = 2021;
const age = currentYear - birthyear;
return age;
}
calcAge(1975);
// Attempt to access "currentYear" and "age" outside of the function scope is not possible
console.log(currentYear); // ReferenceError: currentYear is not defined
console.log(age); // ReferenceError: age is not defined
In the above example, we have a function to calculate the age. However, when trying to print the variables currentYear
and age
, JavaScript throws an error ReferenceError: currentYear is not defined
. This is because the calcAge()
function creates a scope for these variables, which can only be accessed within the function scope.
I hope now you can understand how the function scope works. Let's move on to block scope.
Block Scope
ES6 introduced let
and const
variables. With that, it introduced the block scope. Block scope means that the variables defined inside a code clock {}
can only be used inside it.
For example, a variable created inside an if
statement or for
loop can only be accessed within that code block. Same as function scope, it is not accessible outside of the block scope.
While let
and const
are block scoped, the variables defined with var
have their scope limited to the current function scope or the global scope. Suppose we declare a variable using var
, that variable is accessible outside the block. So, the variable declared using var
within a code block is not block scoped; It is function scoped.
Check the below example,
function calcAge(birthyear) {
const currentYear = 2021;
const age = currentYear - birthyear;
if (age <= 60) {
// Create a variable using "var" inside the block
var working = true;
// Create a variable using "const" inside the block
const message = `Peter is still employed!`;
console.log(message);
}
// Variable created using "var" can be accessed outside the block
console.log(working); // true
// Attempt to access "message" outside of the function scope is not possible
console.log(message); // ReferenceError: message is not defined at calcAge
}
calcAge(1975);
In the above example, we have declared working
using var
and message
using const
. When trying to print the variable message
, JavaScript throws an error ReferenceError: message is not defined at calcAge
. This is because the if
block creates a scope for this variable, which is only accessible within that block scope.
However, there is no error when trying to access working
outside the code block. As explained before, this is because var
is not block scoped, it's function scoped. So you can access working
inside the calcAge()
function since it's the current function scope. But if we try to access the working
outside the calcAge()
function, then JavaScript will throw an error.
Scope can be nested
The scope can be nested, which means you can create functions inside another function, block inside another function, function inside another block, or block inside a block.
The scope contained within another scope is named inner scope. And the scope that wraps another scope is named outer scope.
When there are nested scopes, the inner scope can also access the outer scope variables. But outside of the scopes, these variables are not accessible. So outer scope does not have access to the variables of inner functions or blocks.
Check the below example to understand this behavior.
// Outer function
function calcAge(birthyear) {
const userName = "Peter";
const currentYear = 2021;
const age = currentYear - birthyear;
// Inner block
if (age <= 60) {
const message = `${userName} is still employed!`;
console.log(message);
}
// Inner function
function yearsToRetire() {
const retirement = 60 - age;
console.log(`${userName} will be retired in ${retirement} years!`);
}
yearsToRetire();
}
calcAge(1975);
In the above example, the yearsToRetire()
function and if
block are nested inside the calcAge()
function. To calculate the retirement
, we have accessed the age
variable, which is declared in the outer scope, inside the calcAge()
function.
Also, we have accessed the userName
variable, which is declared in the calcAge()
function scope, in both yearsToRetire()
function and if
block. We can look outwards to access variables in the parent's scope with nested scope. It could be a variable inside an outer function, outer block, or a global variable.
I hope now you have a better understanding of global, function, and block scope. However, before moving to the scope chain, there is one more scope that we should learn, which is lexical scope.
Lexical Scope
Lexical scoping means that organizing and accessing variables are controlled by where we write our functions and code blocks.
For example, a function that is written inside another function has access to the variables of the parent function despite where the function is invoked.
So the lexical scoping means that the scope is defined at the location where the variable or function is defined, and not where they run.
Let's check the below example to understand this.
const userName = "Peter";
function sayUserName() {
console.log(userName);
}
function sayUserNameAgain() {
const userName = "Sarah";
// Invoke the first function
sayUserName();
}
sayUserNameAgain(); // Peter
Let's see what has happened here:
- When the
sayUserNameAgain()
function is called, it creates a local variableuserName
and sets its value asSarah
. - In the next line, the
sayUserName()
function is called, andsayUserName()
function is defined outside thesayUserNameAgain()
function. sayUserName()
function logs theuserName
variable, butuserName
is not defined in thesayUserName()
scope. So we have to go up one scope to the global scope to get the value ofuserName
which isPeter
.- Even though we have
userName = "Sarah"
right above where thesayUserName()
function invokes, we have never accessed that value. - This is because lexical scoping requires us to go where the functions are defined, not where they run.
I hope now you understand what lexical scope is. So let's move on to the scope chain.
Scope Chain
The scope chain is how Javascript looks for variables. When looking for variables through the nested scope, the inner scope first looks at its own scope. If the variable is not assigned locally, which is inside the inner function or block scope, then JavaScript will look at the outer scope of said function or block to find the variable. If Javascript could not find the variable in any of the outer scopes on the chain, it will throw a reference error.
Let's take an example and go through this process step by step. Check the below code.
// Global variable
const userName = "Peter";
// Outer function
function calcAge(birthyear) {
const currentYear = 2021;
const age = currentYear - birthyear;
// inner block
if (age <= 60) {
var working = true;
const message = `Peter is still employed!`;
console.log(message);
}
// inner function
function yearsToRetire() {
const retirement = 60 - age;
console.log(`${userName} will be retired in ${retirement} years!`);
}
yearsToRetire();
}
calcAge(1975);
In the above example,
- We have a global variable called
userName
. - We have an outer function
calcAge()
, which is in the global scope. - We have an inner function,
yearsToRetire()
, nested insidecalcAge()
function. - Also, we have an
if
block inside thecalcAge()
function.
With the above example, let's try to understand how the scope chain works.
First, we have the global scope, which has only one variable, userName
. There is a function declared in the global scope, which is calcAge()
. But to keep things simple, let's focus on the variables. And keep in mind that function and variables work the same way in the scope chain.
If you remember, each function creates its own scope. So inside the global scope, the first function scope is created with the calcAge()
function.
Inside the calcAge()
function there are two variables declared, which are currentYear
and age
. Also, we have access to the global variable userName
inside the calcAge()
function.
If we have any need to access the variable userName
inside this function, then JavaScript looks inside the calcAge()
function to see whether the variable is declared inside the scope. When JavaScript can't find it there, it will reach out to the outer scope, that is the global scope.
Next, inside the first scope, there is a second function, yearsToRetire()
, which also creates its own scope containing the retirement
variable set to 60 - age
. Now we have a nested structure of scopes with one scope inside the other.
We have a string that needs access to userName
variable inside this function. Since JavaScript cannot find this variable within the local scope, it will look up in the scope chain until it finds the variable and uses it.
Also, inside this yearsToRetire()
function scope we also have the access to variables inside the caclAge
function scope, since caclAge
is the parent scope and outer scope of yearsToRetire()
function.
There is an if
block inside the calcAge()
function, which has the two variables declared inside that. However, as I explained earlier, the variable declared with var
is not block scoped. So the variable working
will be a part of the calcAge()
function scope. Since the working
is in the calcAge()
function scope, the yearsToRetire()
function scope also has access to it.
The scope chain applies to block scope as well. Therefore, the if
block scope gets access to all the variables from its outer scope. So the block scope can access the variable inside the calcAge()
function scope and global scope.
Another important thing to remember is that the if
block scope does not have access to any variables in the yearsToRetire()
function scope, and vice versa. The reason for this is lexical scoping.
The way we can access variables depends on where the scope is placed or where it is written in the code. In this scenario, none of these two scopes is written inside one another. We could say that they are sibling scopes since they are both child scopes of the calcAge()
function scope. So, according to the lexical scoping, they cannot access each other's variables. Scope chain only works upwards, not sideways.
So this is how the scope chain works. If one scope needs to use a certain variable but cannot find it in the scope, it will look up in the scope chain and check whether it can find a variable on one of the outer scopes. If the variable is available in the outer scope, then the child scope has the access to it. If it is not there in any outer scopes, the JavaScript will throw a reference error. So this process is called variable lookup.
I hope this post helped you understand the different types of scopes in JavaScript and how the scope chain works.
Happy coding!
This article was originally published on Dasha.
In case you are wondering, Dasha is a conversational-AI-as-a-service platform that lets you embed realistic voice and text conversational capabilities into your apps or products. Start building for free!
Join Dasha Developer Community where you’ll meet welcoming like-minded developers who share ideas, questions, and get all the help they need to build cool conversational AI apps (for free, of course).