Welcome back! š In the previous lesson, we learned what JavaScript is and where it runs. Now, let's dive deeper and understand how JavaScript actually executes your code behind the scenes.
Understanding the execution model is crucial because it helps you:
- Write more efficient code
- Debug issues faster
- Avoid common pitfalls like hoisting bugs
- Understand why certain errors occur
- Master asynchronous programming
Let's pull back the curtain and see what happens when you run JavaScript code!
How JavaScript Code Executes: The Big Picture
When you run a JavaScript program, several things happen in a specific order:
- Code Parsing: JavaScript engine reads and validates your code
- Compilation: Modern engines compile code to optimized machine code (Just-In-Time compilation)
- Execution Context Creation: Environment is set up for code execution
- Code Execution: Your code runs line by line
Key Concept: JavaScript is single-threaded, meaning it can only execute one piece of code at a time. Think of it as a single worker handling tasks one by one.
console.log("First");
console.log("Second");
console.log("Third");
// Output (in order):
// First
// Second
// ThirdExecution Context: The Environment Where Code Runs
Execution Context is the environment in which JavaScript code is executed. Think of it as a container that stores all the information needed to run your code.
Types of Execution Contexts
JavaScript creates different execution contexts:
-
Global Execution Context (GEC)
- Created when your script first runs
- Only one per program
- Creates the global object (
windowin browsers,globalin Node.js) - Sets up the
thiskeyword
-
Function Execution Context (FEC)
- Created every time a function is called
- Each function call gets its own context
- Destroyed after function completes
-
Eval Execution Context (rarely used, avoid in practice)
- Created for code inside
eval()function
- Created for code inside
What's Inside an Execution Context?
Every execution context has two phases:
Phase 1: Creation Phase (Memory Creation)
Before any code runs, JavaScript sets up the environment:
Variable Environment:
- Stores variables and function declarations
- Variables declared with
varare initialized withundefined letandconstare declared but not initialized (Temporal Dead Zone)- Function declarations are stored in their entirety
Scope Chain:
- Reference to outer environment (parent scope)
- Enables access to variables from outer scopes
this Keyword:
- Determined based on how the function is called
// Example of Creation Phase
console.log(name); // undefined (not an error!)
console.log(age); // ReferenceError: Cannot access 'age' before initialization
var name = "Mihir";
let age = 25;
greet(); // "Hello!" (works even before declaration!)
function greet() {
console.log("Hello!");
}Why does this happen?
var nameis hoisted and initialized withundefinedlet ageis hoisted but NOT initialized (Temporal Dead Zone)function greet()is fully hoisted with its body
Phase 2: Execution Phase
Now JavaScript runs your code line by line:
- Assigns actual values to variables
- Executes function calls
- Evaluates expressions
var x = 10; // Assignment happens
let y = 20; // Assignment happens
const z = x + y; // Expression evaluated
console.log(z); // 30Execution Context in Action
Let's trace through a complete example:
var globalVar = "I'm global";
function outer() {
var outerVar = "I'm in outer";
function inner() {
var innerVar = "I'm in inner";
console.log(globalVar); // Can access global
console.log(outerVar); // Can access outer
console.log(innerVar); // Can access inner
}
inner();
}
outer();What happens:
-
Global Execution Context created
globalVardeclared and assigned "I'm global"outerfunction stored in memory
-
outer()is called ā Function Execution Context createdouterVardeclared and assigned "I'm in outer"innerfunction stored in memory
-
inner()is called ā New Function Execution Context createdinnerVardeclared and assigned "I'm in inner"- All three
console.logstatements execute - Can access variables from all three scopes!
-
inner()completes ā Its context is destroyed -
outer()completes ā Its context is destroyed -
Program ends ā Global context remains until page/script ends
The Call Stack: JavaScript's Task Manager
The call stack is a data structure that tracks which function is currently executing and what functions are waiting to run.
How the Call Stack Works
Think of the call stack like a stack of plates:
- You can only add plates to the top (push)
- You can only remove plates from the top (pop)
- Last plate added is the first one removed (LIFO - Last In, First Out)
function first() {
console.log("Inside first");
second();
console.log("Back in first");
}
function second() {
console.log("Inside second");
third();
console.log("Back in second");
}
function third() {
console.log("Inside third");
}
first();Call Stack Visualization:
Step 1: first() is called
āāāāāāāāāāā
ā first() ā
āāāāāāāāāāā
Step 2: second() is called from first()
āāāāāāāāāāāā
ā second() ā
āāāāāāāāāāāā¤
ā first() ā
āāāāāāāāāāāā
Step 3: third() is called from second()
āāāāāāāāāāā
ā third() ā
āāāāāāāāāāā¤
āsecond() ā
āāāāāāāāāāā¤
āfirst() ā
āāāāāāāāāāā
Step 4: third() completes and is popped
āāāāāāāāāāāā
ā second() ā
āāāāāāāāāāāā¤
ā first() ā
āāāāāāāāāāāā
Step 5: second() completes and is popped
āāāāāāāāāāā
ā first() ā
āāāāāāāāāāā
Step 6: first() completes and is popped
(Stack is empty)Output:
Inside first
Inside second
Inside third
Back in second
Back in firstStack Overflow Error
The call stack has a limited size. If you create too many nested function calls, you'll get a stack overflow error.
function recursiveFunction() {
recursiveFunction(); // Calls itself infinitely
}
recursiveFunction();
// RangeError: Maximum call stack size exceededWhy it happens:
- Each call adds to the stack
- No base case to stop recursion
- Stack runs out of memory
Fixed version with base case:
function countdown(n) {
if (n <= 0) {
console.log("Done!");
return; // Base case - stops recursion
}
console.log(n);
countdown(n - 1); // Recursive call
}
countdown(5);
// Output: 5, 4, 3, 2, 1, Done!Hoisting: Why You Can Use Things Before Declaration
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the creation phase.
Important: Only Declarations Are Hoisted, Not Initializations
// What you write:
console.log(name); // undefined
var name = "Mihir";
// How JavaScript interprets it:
var name; // Declaration hoisted
console.log(name); // undefined
name = "Mihir"; // Assignment stays in placeHoisting with var, let, and const
var - Hoisted and Initialized:
console.log(x); // undefined (no error)
var x = 10;
console.log(x); // 10let and const - Hoisted but NOT Initialized (Temporal Dead Zone):
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;
console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 30;Temporal Dead Zone (TDZ): The period between the start of the scope and the actual declaration where the variable exists but can't be accessed.
{
// TDZ starts
console.log(name); // ReferenceError
// TDZ continues
// TDZ continues
let name = "Mihir"; // TDZ ends
console.log(name); // "Mihir" - works fine
}Function Hoisting
Function Declarations - Fully Hoisted:
greet(); // "Hello!" - Works!
function greet() {
console.log("Hello!");
}Function Expressions - NOT Hoisted:
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
// Why? Because only the variable declaration is hoisted:
// var sayHi; // undefined
// sayHi(); // undefined() - Error!
// sayHi = function... // Assignment happens laterArrow Functions - NOT Hoisted:
greeting(); // ReferenceError
const greeting = () => {
console.log("Hey!");
};Best Practices to Avoid Hoisting Issues
- Always declare variables at the top of their scope
function example() {
let x = 10; // Declare first
let y = 20;
let z = x + y;
console.log(z);
}- Use
letandconstinstead ofvar
// Bad
var name = "Mihir";
// Good
const name = "Mihir"; // If value won't change
let age = 25; // If value will change- Define functions before calling them
// Good practice
function calculate() {
return 42;
}
const result = calculate();Scope and Scope Chain: Variable Access Rules
Scope determines where variables are accessible in your code.
Types of Scope
1. Global Scope
- Variables declared outside any function
- Accessible everywhere in your code
- Avoid polluting global scope
const globalVar = "I'm global";
function test() {
console.log(globalVar); // Accessible here
}
test();
console.log(globalVar); // Accessible here too2. Function Scope (Local Scope)
- Variables declared inside a function
- Only accessible within that function
- Created with
var,let, orconst
function myFunction() {
var functionVar = "I'm local";
console.log(functionVar); // Works
}
myFunction();
console.log(functionVar); // ReferenceError: functionVar is not defined3. Block Scope
- Variables declared inside
{ }blocks - Only
letandconstare block-scoped varis NOT block-scoped
if (true) {
var x = 10; // Function-scoped
let y = 20; // Block-scoped
const z = 30; // Block-scoped
}
console.log(x); // 10 - accessible (var ignores block)
console.log(y); // ReferenceError (let respects block)
console.log(z); // ReferenceError (const respects block)4. Lexical Scope (Static Scope)
- Inner functions have access to outer function variables
- Determined by where functions are written, not where they're called
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(outerVar); // Can access outer variable
console.log(innerVar); // Can access own variable
}
inner();
console.log(innerVar); // ReferenceError - can't access inner variable
}
outer();The Scope Chain
When JavaScript looks for a variable, it follows the scope chain:
- Look in the current scope
- If not found, look in the parent scope
- Keep going up until reaching global scope
- If still not found, throw a ReferenceError
const global = "global";
function level1() {
const level1Var = "level1";
function level2() {
const level2Var = "level2";
function level3() {
const level3Var = "level3";
console.log(level3Var); // Found in level3
console.log(level2Var); // Found in level2 (parent)
console.log(level1Var); // Found in level1 (grandparent)
console.log(global); // Found in global scope
}
level3();
}
level2();
}
level1();Scope Chain Visualization:
level3() ā level2() ā level1() ā Global ScopeVariable Shadowing
When inner scope declares a variable with the same name as an outer variable:
const name = "Global Mihir";
function test() {
const name = "Local Mihir"; // Shadows global name
console.log(name); // "Local Mihir"
}
test();
console.log(name); // "Global Mihir"The Event Loop: Handling Asynchronous Code
JavaScript is single-threaded but can handle asynchronous operations thanks to the event loop.
The Problem: Blocking Code
console.log("Start");
// This would block for 3 seconds
// (if we had a blocking sleep function)
sleep(3000);
console.log("End");If JavaScript were purely synchronous, the browser would freeze for 3 seconds. Not good!
The Solution: Asynchronous Execution
JavaScript uses:
- Call Stack: For synchronous code
- Web APIs: Browser-provided features (timers, HTTP requests, DOM events)
- Callback Queue: Holds callbacks ready to execute
- Event Loop: Checks if call stack is empty and moves callbacks from queue to stack
How the Event Loop Works
Components:
- Call Stack: Executes code
- Web APIs: Handle async operations (setTimeout, fetch, DOM events)
- Callback Queue (Task Queue): Stores callbacks from completed async operations
- Microtask Queue: Higher priority queue for Promises
- Event Loop: Continuously checks and moves tasks
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");Output:
Start
End
Promise
TimeoutWhy this order?
Step-by-step execution:
console.log("Start")ā Call Stack ā Output: "Start"setTimeout()ā Sent to Web API ā Callback registeredPromise.resolve()ā Microtask Queue ā Callback queuedconsole.log("End")ā Call Stack ā Output: "End"- Call Stack is empty ā Event Loop checks queues
- Microtask Queue has priority ā Promise callback executes ā Output: "Promise"
- Callback Queue ā setTimeout callback executes ā Output: "Timeout"
Visual Representation
āāāāāāāāāāāāāāāāāāā
ā Call Stack ā ā Executes code
āāāāāāāāāāāāāāāāāāā
ā
ā
Event Loop (checks if empty)
ā
ā
āāāāāāāāāāāāāāāāāāā
ā Microtask Queue ā ā Promises (higher priority)
āāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāā
ā Callback Queue ā ā setTimeout, setInterval, DOM events
āāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāā
ā Web APIs ā ā Browser features (timers, fetch, etc.)
āāāāāāāāāāāāāāāāāāāReal-World Example: setTimeout
console.log("1");
setTimeout(() => {
console.log("2");
}, 1000);
console.log("3");
// Output:
// 1
// 3
// (1 second pause)
// 2What happens:
- "1" logs immediately
- setTimeout sends callback to Web API (starts 1-second timer)
- "3" logs immediately (doesn't wait for timer)
- After 1 second, callback moves to Callback Queue
- Event Loop moves callback to Call Stack
- "2" logs
Why setTimeout(fn, 0) Doesn't Run Immediately
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
// Output: A, C, B (not A, B, C!)Even with 0ms delay, the callback goes through the event loop:
- Synchronous code runs first (A, C)
- Then event loop processes async callbacks (B)
Putting It All Together: Complete Example
Let's trace through a complex example:
const name = "Mihir";
function first() {
console.log("First function start");
second();
console.log("First function end");
}
function second() {
console.log("Second function start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
Promise.resolve().then(() => {
console.log("Promise callback");
});
console.log("Second function end");
}
console.log("Script start");
first();
console.log("Script end");Output:
Script start
First function start
Second function start
Second function end
First function end
Script end
Promise callback
Timeout callbackDetailed Execution Trace:
- Global Execution Context created
- "Script start" ā logged
first()called ā Function Context created ā pushed to Call Stack- "First function start" ā logged
second()called ā New Function Context ā pushed to Call Stack- "Second function start" ā logged
setTimeout()ā sent to Web APIPromise.resolve()ā callback to Microtask Queue- "Second function end" ā logged
second()completes ā popped from Call Stack- "First function end" ā logged
first()completes ā popped from Call Stack- "Script end" ā logged
- Call Stack empty ā Event Loop checks queues
- Microtask Queue: "Promise callback" ā logged
- Callback Queue: "Timeout callback" ā logged
Key Takeaways
Understanding JavaScript's execution model helps you:
ā Execution Context
- Created in two phases: Creation (memory setup) and Execution (code runs)
- Variables, functions, and
thisare set up before code executes
ā Call Stack
- Tracks function execution using LIFO (Last In, First Out)
- Stack overflow occurs with infinite recursion
ā Hoisting
- Declarations are moved to the top during creation phase
varis initialized withundefined,let/constremain uninitialized (TDZ)- Function declarations are fully hoisted
ā Scope and Scope Chain
- Global, function, and block scope determine variable accessibility
- JavaScript searches through scope chain to find variables
- Lexical scoping: inner functions access outer variables
ā Event Loop
- Enables asynchronous programming in single-threaded JavaScript
- Microtask Queue (Promises) has higher priority than Callback Queue (setTimeout)
- Event loop moves callbacks to call stack when it's empty
Practice Questions
Test your understanding with these questions:
Question 1
console.log(x);
var x = 5;What will be logged and why?
Answer: undefined will be logged because var x is hoisted and initialized with undefined during the creation phase, but the assignment x = 5 happens later during the execution phase.
Question 2
function outer() {
var a = 10;
function inner() {
console.log(a);
}
return inner;
}
const fn = outer();
fn();What will be logged and why?
Answer: 10 will be logged. The inner function has access to outer's variable a through closure and the scope chain, even after outer() has finished executing. This is because the inner function maintains a reference to its parent scope.
Question 3
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");What is the output order?
Answer: The output will be: A, D, C, B
Explanation:
- Synchronous code executes first: A, D
- Then the Microtask Queue (Promises) executes: C
- Finally the Callback Queue (setTimeout) executes: B
Even though setTimeout has 0ms delay, it still goes through the event loop and has lower priority than Promises.
What's Next?
Congratulations! š You now understand how JavaScript executes code behind the scenes.
In the next lesson, we'll set up your development environment by installing Node.js. You'll learn:
- What Node.js is and why developers use it
- How to install Node.js on your system
- Understanding npm (Node Package Manager)
- Running JavaScript files outside the browser
- Setting up a professional development workflow
This is an essential step before we dive into JavaScript fundamentals!