Understanding JavaScript Scope


JavaScript is a powerful and versatile programming language that executes code in distinct phases while adhering to specific scoping rules. Understanding JavaScript’s scope is essential for writing efficient, maintainable, and bug-free code. In this blog, we will explore key concepts such as execution phases, different types of scope, and other important aspects that influence how JavaScript manages variables and function execution.
Phases of Execution in JavaScript
JavaScript code execution occurs in three main phases:
Parsing Phase:
Converts the code into an Abstract Syntax Tree (AST).
Breaks down each line of code into meaningful components.
Identifies syntax errors before execution.
Compilation & Scope Resolution Phase:
Determines variable and function scope.
Establishes the scope chain for resolving variable references.
Execution Phase:
- Executes the code, assigns values to variables, and invokes functions.
Execution Context and Scopes in JavaScript
An execution context is an abstract concept that holds information about the environment within which the JavaScript code is executed. Every time a script or function runs, an execution context is created, determining which variables, objects, and functions are accessible during the code's execution.
Types of Execution Contexts:
Global Execution Context:
This is the default or base context. When the JavaScript engine starts executing your code, it first creates the Global Execution Context.
Variables and functions that are not inside any function are placed in the Global Execution Context.
There is only one Global Execution Context in any JavaScript program.
Function Execution Context:
Every time a function is called, a new Function Execution Context is created for that function.
It contains the function’s own local variables, arguments object,
this
keyword, and more.A Function Execution Context is created whenever a function is invoked and can be nested within other execution contexts.
Eval Execution Context:
Created when
eval()
is called.Generally avoided due to performance and security concerns.
Each execution context has two stages:
Creation Phase:
Memory is allocated for variables and functions, the scope chain is established, and the
this
keyword is defined (not in arrow functions).Hoisting occurs, meaning variables declared with
var
are set toundefined
, and function declarations are loaded into memory.
Execution Phase:
The code is executed line by line.
Values are assigned to variables, and functions are executed.
Scopes in JavaScript
Scopes determine where variables or functions are organized and accessible in the code. JavaScript's scoping mechanism is different from other languages like Java, C++, and Python, so avoid mixing these concepts.
Types of Scope in JavaScript
Global Scope
Function Scope
Block Scope
1. Global Scope
Variables declared outside any function or block are in the global scope. These variables are accessible from anywhere in your code, whether inside a function, loop, or conditional statement.
Example:
let country = "India";
function showCountry() {
console.log(country); // Accessible here
}
showCountry(); // Outputs: India
console.log(country); // Accessible here too
In the above code, country
is declared in the global scope, so it can be accessed both inside and outside the showCountry
function.
2. Function Scope
When a variable is declared inside a function, it is scoped to that function, meaning it can only be accessed within that function.
Example:
function showCity() {
let city = "Mumbai";
console.log(city); // Accessible here
}
showCity(); // Outputs: Mumbai
console.log(city); // Error: city is not defined
Here, city
is declared inside the showCity
function, so it is only accessible within that function and not outside.
3. Block Scope
Block scope is created when variables are declared inside a pair of curly braces {}
. This can happen within loops, conditionals, or just standalone blocks.
Example:
if (true) {
let festival = "Diwali";
console.log(festival); // Accessible here
}
console.log(festival); // Error: festival is not defined
In this example, festival
is declared inside an if
block, so it is only accessible within that block.
Variable Declarations and Scope
In JavaScript, variables can be declared using var
, let
, or const
. The way a variable is declared affects its scope.
Any variable is used only in two ways:
RHS (Right-Hand Side): When we consume the variable.
LHS (Left-Hand Side): When we assign a value or declare the variable.
var
, let
, and const
var
Variables declared with var
are either function-scoped or globally scoped. var
does not support block scope.
Example:
function showVar() {
if (true) {
var festival = "Diwali";
}
console.log(festival); // Accessible here, even though it's inside a block
}
showVar(); // Outputs: "Diwali"
Another Example:
var city = "Mumbai";
console.log(city, state); // Output: Mumbai undefined
if (true) {
var city = "Delhi";
var state = "Maharashtra";
console.log(city, state); // Output: Delhi Maharashtra
}
console.log(city, state); // Output: Delhi Maharashtra
In the above example, during the scope resolution phase, both city
and state
are declared in the global scope due to the use of var
. When the execution phase starts:
city
has been initialized toMumbai
.state
is declared in the global scope but has not been assigned a value yet, so it isundefined
.
Within the if
block, city
and state
are reassigned:
city
is updated toDelhi
.state
is assigned the valueMaharashtra
.
Since var
has function or global scope, the changes to city
and state
in the if
block affect their values globally. Thus, when the final console.log(city, state);
is executed, both city
and state
have the values Delhi
and Maharashtra
, respectively.
| Note: How is function scope different from block scope?
A variable with function scope has a unique characteristic: it can be defined anywhere within the function but will still be accessible throughout the entire function.
function electionYear() {
console.log("Upcoming election year is", year); // undefined
var year = 2024;
console.log("Upcoming election year is", year); // 2024
}
If we try to do the same thing using let
instead of var
, it will result in an error because let
does not have function scope like var
does.
| Note: Automatically Global:
This refers to variables that are automatically added to the global scope in certain situations. Typically, this occurs when you create a variable inside a function without using the var
, let
, or const
keyword. These variables become global, even if they are defined inside a function.
var primeMinister = "Atal Bihari Vajpayee"; // Declared globally
function updatePM() {
primeMinister = "Narendra Modi"; // Modifies the global variable
currency = "Rupee"; // 'currency' is not declared with var/let/const, so it becomes a global variable
}
console.log("Current Prime Minister:", primeMinister); // Atal Bihari Vajpayee
// console.log("Currency:", currency); // Error (not yet defined)
updatePM(); // Call function to modify `primeMinister` and declare `currency`
console.log("Current Prime Minister:", primeMinister); // Narendra Modi
console.log("Currency:", currency); // Rupee
Automatic global variables can cause issues in JavaScript. To prevent this, you can enable strict mode by adding the following at the top of your script:
"use strict";
let
and const
:
These keywords support block scope, meaning variables declared inside a block are only accessible within that block.
Example:
function showLetConst() {
if (true) {
let state = "West Bengal";
const capital = "Kolkata";
console.log(state, capital); // Accessible inside the block
}
console.log(state, capital); // Error: `state` and `capital` are not defined outside the block
}
showLetConst();
Special Characteristics of let
and const
- Block Scope Inside Functions: Variables declared with
let
orconst
are not [hoisted](## Hoisting) in the same way asvar
. They are only accessible after their declaration, which differs from the function scope provided byvar
. This difference leads to the concept of the Temporal Dead Zone (TDZ).
Example:
function checkLet() {
console.log(festival); // Error: Cannot access 'festival' before initialization
let festival = "Holi";
}
checkLet();
Here, festival
is in the TDZ from the start of the block until the declaration is encountered, making it inaccessible before its declaration.
Temporal Dead Zone (TDZ): This is the region of the block before the variable is declared. A variable declared with
let
,const
, orclass
is in the TDZ from the start of the block until the code execution reaches its declaration.No Redeclaration: Variables declared with
let
andconst
cannot be redeclared in the same scope. This is enforced during the first phase (compilation phase) of the JavaScript execution process.
Example:
let independenceYear = 1947;
let independenceYear = 1857; // Error: Identifier 'independenceYear' has already been declared
Variable Shadowing & Illegal Shadowing
Variable Shadowing
Variable shadowing happens when a variable in an inner scope (inside {}
) has the same name as a variable in an outer scope. The inner variable "shadows" the outer one within its block.
Example:
function showCapital() {
let capital = "Delhi"; // Outer scope
if (true) {
let capital = "Mumbai"; // Inner scope shadows the outer variable
console.log("Inside block:", capital); // Prints: Mumbai
}
console.log("Outside block:", capital); // Prints: Delhi (outer variable remains unchanged)
}
showCapital();
Illegal Shadowing
Illegal shadowing happens when we try to shadow a let
variable using var
, which JavaScript does not allow. The reason is that var
is function-scoped, while let
is block-scoped. If var
is declared inside a block, it "escapes" the block due to its function scope, causing a conflict with the existing let
variable.
function festivalSeason() {
let festival = "Diwali";
if (true) {
var festival = "Holi"; // ❌ Not allowed! 'var' cannot shadow 'let'
console.log(festival);
}
}
festivalSeason();
🔴 Error: SyntaxError: Identifier 'festival' has already been declared
Lexical Scoping / Lexical Parsing
JavaScript uses lexical scoping, also known as static scoping. In lexical scoping, the scope of variables is determined during compile time. Although variable values are assigned during the execution phase, the scope of each variable is defined during the compilation phase. Therefore, the rules for accessing variables are based on the location where functions and blocks are written in the code.
| Scope Chaining: Scope chaining is the process by which JavaScript looks up variable values in the current scope and, if not found, continues searching in the outer scopes, following the chain of scopes until the variable is found or the global scope is reached. This is possible because of lexical scoping, where nested functions have access to variables in their outer functions.
Example:
function outerFunction() {
let country = 'India';
function innerFunction() {
console.log(country); // Accessible due to lexical scoping
}
innerFunction();
}
outerFunction(); // Outputs: India
In the above code, innerFunction
can access country
because it is lexically within the scope of outerFunction
. If country
were not found in the immediate scope of innerFunction
, JavaScript would check the next outer scope (in this case, outerFunction
's scope) to find it, forming a chain of scopes.
In summary, scope chaining allows JavaScript to search through the chain of scopes to find variables, starting from the innermost scope and moving outward. This behavior is a fundamental aspect of lexical scoping.
Closures
A closure provides access to the reference of variables from its parent function, even after that parent function has returned. The function keeps a reference to its outer scope, preserving the scope chain over time. This means that the variable environment of the execution context in which the function was created remains accessible even after that execution context has finished.
Example:
const bookTrainTicket = function () {
let ticketsBooked = 0;
return function () {
ticketsBooked++;
console.log(`🚆 ${ticketsBooked} ticket(s) booked for Indian Railways`);
};
};
const ticketCounter = bookTrainTicket();
ticketCounter(); // 🚆 1 ticket(s) booked for Indian Railways
ticketCounter(); // 🚆 2 ticket(s) booked for Indian Railways
ticketCounter(); // 🚆 3 ticket(s) booked for Indian Railways
This example demonstrates how the inner function retains access to ticketsBooked
, even after bookTrainTicket
has completed its execution.
Closures Store References, Not Values
A closure does not store the variable value but a reference to the variable. So, if the variable value changes before the function is returned, the updated value will be used at the time of execution.
Example:
const planWedding = function () {
let guestsCount = 150; // Initial guest count for the wedding
function displayGuestCount() {
console.log(`${guestsCount} guests invited for the wedding.`);
}
// After adding more guests
guestsCount += 50;
return displayGuestCount;
};
const finalGuestList = planWedding();
finalGuestList(); // 200 guests invited for the wedding.
Closures Can Access Variables from Multiple Levels Up
A closure can access not only its immediate parent’s variables but also variables from higher-level parent functions.
Example:
function sweetShop() {
let totalSweets = 100; // Initial number of sweets in the shop
// Function to handle sweet orders
const orderSweets = function (sweetsOrdered) {
totalSweets -= sweetsOrdered; // Reduce the sweets in stock by the ordered amount
function displayStock() {
console.log(`${totalSweets} sweets left in the shop.`);
}
// Call displayStock to show the updated sweets count
displayStock();
};
return orderSweets;
}
const shop = sweetShop(); // Create the sweet shop
// Simulating some orders
shop(20); // 80 sweets left in the shop.
shop(10); // 70 sweets left in the shop.
Hoisting
Hoisting is a term commonly used in the JavaScript community, although it is not officially defined in the ECMAScript specification. It refers to a behavior in JavaScript's scoping mechanism, which occurs in two main phases of code execution: the Compilation and Scope Resolution Phase and the Interpretation or Execution Phase. During the compilation phase, many variables are already identified, so when the code is executed, it seems as JavaScript is aware of these variables or functions even before their actual declaration. This phenomenon, where the interpreter appears to move the declarations of functions, variables, classes, or imports to the top of their scope before execution, is known as hoisting.
- Example of Hoisting with
var
andlet
:
function hoistExample() {
console.log(city); // undefined (var is hoisted and initialized with undefined)
// console.log(state); // ❌ ReferenceError (let is in the Temporal Dead Zone)
var city = "Kolkata";
let state = "West Bengal";
console.log(city); // Kolkata
console.log(state); // West Bengal
}
hoistExample();
In this example:
var city
is hoisted, soconsole.log(city);
outputsundefined
because its value is not assigned until the linevar city = "Kolkata";
.let state
is also hoisted, but it is in the Temporal Dead Zone (TDZ) from the start of the block until the declaration is encountered. Accessingstate
before its declaration throws a ReferenceError because variables declared withlet
are not initialized until the execution reaches thelet
statement.Example of Hoisting with a
Function Declaration
:
console.log(getTrainFare(500, 2)); // 1000 Rupees
function getTrainFare(price, passengers) {
return `${price * passengers} Rupees`;
}
In this example, the function getTrainFare
is fully hoisted, allowing it to be called before its declaration in the code.
- Hoisting with
Function Expressions
:
If a function expression is created using const
or let
, the function itself is not available before its declaration because the variable is hoisted but remains in the Temporal Dead Zone (TDZ). If a function expression is created using var
, it is hoisted but initialized with undefined
. If you try to call the function before its definition, it will throw a TypeError
, stating that the function is not a function
, because the code attempts to call undefined()
.
console.log(bookFlight(3)); // TypeError: bookFlight is not a function
var bookFlight = function (seats) {
return `✈️ ${seats} seat(s) booked for Air India`;
};
Conclusion
Understanding JavaScript scope and closures is essential for writing efficient and bug-free code. By leveraging lexical scoping, hoisting, and closures, you can better manage variables, avoid unintended global state modifications, and write cleaner code.
Mastering these concepts will enhance your ability to write scalable and maintainable JavaScript applications. As you continue your JavaScript journey, practice using these concepts to gain a deeper understanding of how JavaScript code works. The more you experiment, the more confident you'll become in handling scope and closures effectively. Happy coding!
Wrapping Up
Thank you for reading Understanding JavaScript Scope! I hope you found this article informative and valuable.
If you have any suggestions or feedback, feel free to share your thoughts in the comments section.
Subscribe to my newsletter
Read articles from Soumadip Majila directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
