⚑ JavaScript Fundamentals

Deep dive into Execution Context, Hoisting, Closures, this, Prototypes, ES6 Classes & Inheritance

πŸ“¦ Execution Context

The Execution Context is the environment where JavaScript code is evaluated and executed. It's the foundation for understanding closures, hoisting, scope, and this.

What's Inside an Execution Context?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EXECUTION CONTEXT β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 1. Variable Environment β”‚ β”‚ β€’ Variables (var, let, const) β”‚ β”‚ β€’ Function declarations β”‚ β”‚ β€’ Arguments object (for functions) β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 2. Lexical Environment β”‚ β”‚ β€’ Reference to outer environment (parent scope) β”‚ β”‚ β€’ Enables scope chain lookups β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 3. This Binding β”‚ β”‚ β€’ What `this` refers to in this context β”‚ β”‚ β€’ Determined by HOW the function is called β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Types of Execution Context

TypeDescriptionWhen Created
GlobalCreated when JS file starts, only ONE per programScript starts
FunctionCreated every time a function is calledFunction invoked
EvalCreated when eval() is used (avoid using)eval() called

Two Phases of Execution Context

Phase 1: Creation (Memory Allocation)

  • Memory allocated for variables & functions
  • var β†’ initialized to undefined
  • let/const β†’ uninitialized (TDZ)
  • Function declarations β†’ stored entirely in memory
  • this binding determined

Phase 2: Execution (Code Runs)

  • Code executed line by line
  • Variables assigned actual values
  • Functions called (new contexts created)
  • Expressions evaluated
  • Callables invoked

Visualizing Both Phases

var name = "Sandeep";
let age = 25;

function greet() {
    console.log("Hello " + name);
}

greet();

// ═══════════════════════════════════════════════════════════════
// PHASE 1: CREATION PHASE (Before any code runs)
// ═══════════════════════════════════════════════════════════════
// 
// Global Execution Context Memory:
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ name          β†’ undefined           β”‚  (var gets undefined)
// β”‚ age           β†’ <uninitialized>     β”‚  (let is in TDZ)
// β”‚ greet         β†’ function(){...}     β”‚  (full function stored)
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// 
// ═══════════════════════════════════════════════════════════════
// PHASE 2: EXECUTION PHASE (Code runs line by line)
// ═══════════════════════════════════════════════════════════════
// 
// Line 1: name = "Sandeep"
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ name          β†’ "Sandeep"           β”‚  (updated!)
// β”‚ age           β†’ <uninitialized>     β”‚
// β”‚ greet         β†’ function(){...}     β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// 
// Line 2: age = 25
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ name          β†’ "Sandeep"           β”‚
// β”‚ age           β†’ 25                  β”‚  (now accessible!)
// β”‚ greet         β†’ function(){...}     β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// 
// Line 8: greet() is called β†’ New Function Execution Context!

The Call Stack

The Call Stack is how JavaScript keeps track of which execution context is running. Only ONE executes at a time!

function first() {
    console.log("First - start");
    second();
    console.log("First - end");
}

function second() {
    console.log("Second - start");
    third();
    console.log("Second - end");
}

function third() {
    console.log("Third");
}

first();

// Output:
// First - start
// Second - start
// Third
// Second - end
// First - end

// ═══════════════════════════════════════════════════════════════
// Call Stack Visualization (step by step)
// ═══════════════════════════════════════════════════════════════
//
// Step 1: first() called
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ first() Context β”‚
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ Global Context  β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//
// Step 2: second() called (inside first)
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ second() Contextβ”‚
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ first() Context β”‚  ← Paused, waiting
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ Global Context  β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//
// Step 3: third() called (inside second)
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ third() Context β”‚  ← Currently executing (top of stack)
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ second() Contextβ”‚  ← Paused, waiting
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ first() Context β”‚  ← Paused, waiting
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ Global Context  β”‚  ← Always at bottom
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//
// Step 4: third() finishes β†’ POPPED off stack, memory freed!
// Step 5: second() resumes β†’ finishes β†’ POPPED
// Step 6: first() resumes β†’ finishes β†’ POPPED
Key Insight:
  • Creation Phase = Memory allocation for all declarations
  • Execution Phase = Running code line by line
  • Only ONE execution context runs at a time (JavaScript is single-threaded)

πŸ”„ Hoisting & Temporal Dead Zone

Hoisting is JavaScript's behavior where variable and function declarations are processed during the Creation Phase, before any code executes.

⚠️ Important Clarification: "Moving to the top" is just a metaphor! Nothing actually moves. What really happens is memory allocation during the Creation Phase.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ THE REALITY β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ Creation Phase: β”‚ β”‚ β€’ JS engine scans ALL declarations (var, let, const, function) β”‚ β”‚ β€’ Allocates memory for them β”‚ β”‚ β€’ var β†’ gets undefined β”‚ β”‚ β€’ let/const β†’ marked as "uninitialized" (TDZ) β”‚ β”‚ β€’ function declarations β†’ entire function stored β”‚ β”‚ β”‚ β”‚ Execution Phase: β”‚ β”‚ β€’ Code runs line by line β”‚ β”‚ β€’ Variables get their actual values when assignment is reached β”‚ β”‚ β”‚ β”‚ So "hoisting" = memory allocation happens in creation phase β”‚ β”‚ NOT = code physically moving β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Hoisting Behavior by Type

DeclarationHoisted?Initial ValueAccessible Before?
varβœ… Yesundefinedβœ… Yes (returns undefined)
letβœ… YesUninitialized❌ TDZ Error
constβœ… YesUninitialized❌ TDZ Error
function declarationβœ… YesEntire functionβœ… Fully works!
function expressionAs variableSame as var/let/constSame as variable type
class declarationβœ… YesUninitialized❌ TDZ Error

var Hoisting - The Classic Example

console.log(message);  // undefined (NOT an error!)
var message = "Hello World";
console.log(message);  // "Hello World"

// ═══════════════════════════════════════════════════════════════
// What actually happens:
// ═══════════════════════════════════════════════════════════════
//
// CREATION PHASE:
// Memory: { message: undefined }
//
// EXECUTION PHASE:
// Line 1: console.log(message) β†’ prints undefined (already in memory!)
// Line 2: message = "Hello World" β†’ updates memory
// Line 3: console.log(message) β†’ prints "Hello World"

Why var Gets undefined But let/const Get Errors

// This is a DESIGN CHOICE by JavaScript:

// var - Legacy behavior (ES5 and before)
// Initialize to undefined for "safety" - prevents crashes, but hides bugs
console.log(a);  // undefined
var a = 10;

// let/const - Modern behavior (ES6)
// Leave uninitialized to CATCH bugs early - fail fast philosophy
console.log(b);  // ReferenceError: Cannot access 'b' before initialization
let b = 20;

// The error message says "before initialization" NOT "b is not defined"
// This proves let IS hoisted (exists) but can't be accessed yet!

Temporal Dead Zone (TDZ)

The TDZ is the period between entering a scope and the actual declaration line.

{ // Block starts here β”‚ β”‚ ← TDZ STARTS (variable exists in memory but can't be accessed) β”‚ β”‚ console.log(age); // ReferenceError! Still in TDZ β”‚ console.log(age); // ReferenceError! Still in TDZ β”‚ console.log(age); // ReferenceError! Still in TDZ β”‚ β”‚ ← TDZ ENDS here β–Ό let age = 25; // Declaration + Initialization console.log(age); // 25 (works fine now!) }
// TDZ in action - tricky example
let x = 10;

function example() {
    console.log(x);  // ReferenceError!
    let x = 20;      // This shadows outer x, but TDZ applies
}

example();

// You might expect 10 (from outer x), but NO!
// The inner let x creates TDZ from function start to declaration line
// So accessing x before line 5 throws ReferenceError

Function Hoisting - The Special Case

// Function DECLARATIONS are fully hoisted!
greet();  // "Hello!" - Works BEFORE declaration!

function greet() {
    console.log("Hello!");
}

// ═══════════════════════════════════════════════════════════════
// Why? In creation phase:
// Memory: { greet: function greet() { console.log("Hello!"); } }
// The ENTIRE function is stored, not just the name
// ═══════════════════════════════════════════════════════════════
// Function EXPRESSIONS follow their variable type!

sayHi();  // TypeError: sayHi is not a function

var sayHi = function() {
    console.log("Hi!");
};

// In creation phase:
// Memory: { sayHi: undefined }
// Calling undefined() throws TypeError

// ═══════════════════════════════════════════════════════════════

sayHello();  // ReferenceError: Cannot access 'sayHello' before initialization

let sayHello = function() {
    console.log("Hello!");
};

// In creation phase:
// Memory: { sayHello: <uninitialized> }
// Accessing before declaration triggers TDZ error

The Classic Loop Gotcha (var vs let)

// ═══════════════════════════════════════════════════════════════
// Problem: Using var in loops with callbacks
// ═══════════════════════════════════════════════════════════════

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

// Expected: 0, 1, 2
// Actual:   3, 3, 3

// WHY?
// var is FUNCTION-scoped, not block-scoped
// Only ONE variable 'i' exists for the entire loop
// By the time setTimeout callbacks run, the loop is done
// And i = 3 (the exit condition)

// ═══════════════════════════════════════════════════════════════
// Solution 1: Use let (block-scoped)
// ═══════════════════════════════════════════════════════════════

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

// Output: 0, 1, 2 βœ“
// Each iteration gets its OWN 'i' variable (block scope)

// ═══════════════════════════════════════════════════════════════
// Solution 2: IIFE (Immediately Invoked Function Expression)
// ═══════════════════════════════════════════════════════════════

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 100);
    })(i);  // Pass current i as j, creating new closure scope
}

// Output: 0, 1, 2 βœ“

πŸ”— Lexical Environment & Scope Chain

Lexical means "relating to the words/text" - in programming, it means where the code is physically written.

Lexical Environment = Local Memory + Reference to Parent Environment

Scope Chain Lookup

When JavaScript needs a variable: 1. Look in CURRENT scope ↓ Not found? 2. Look in PARENT scope ↓ Not found? 3. Look in GRANDPARENT scope ↓ Keep going up... 4. Look in GLOBAL scope ↓ Not found? 5. ReferenceError: variable is not defined

Complete Scope Chain Example

var globalVar = "I'm global";

function outer() {
    var outerVar = "I'm in outer";
    
    function inner() {
        var innerVar = "I'm in inner";
        
        // Scope chain lookup in action:
        console.log(innerVar);   // "I'm in inner"  ← Found in own scope
        console.log(outerVar);   // "I'm in outer"  ← Found in parent (outer)
        console.log(globalVar);  // "I'm global"    ← Found in grandparent (global)
    }
    
    inner();
}

outer();

// ═══════════════════════════════════════════════════════════════
// Visualizing the Scope Chain:
// ═══════════════════════════════════════════════════════════════
//
//                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//                     β”‚   Global Scope       β”‚
//                     β”‚   globalVar = "..."  β”‚
//                     β”‚   outer = function   β”‚
//                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//                                β”‚ parent reference
//                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//                     β”‚   outer() Scope      β”‚
//                     β”‚   outerVar = "..."   β”‚
//                     β”‚   inner = function   β”‚
//                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//                                β”‚ parent reference
//                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//                     β”‚   inner() Scope      β”‚
//                     β”‚   innerVar = "..."   β”‚
//                     β”‚   (lookup starts)    β”‚
//                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Lexical Scope = WHERE Code is Written (Not Where Called!)

var name = "Global";

function printName() {
    console.log(name);  // Which name will this print?
}

function wrapper() {
    var name = "Local";
    printName();  // Calling printName from inside wrapper
}

wrapper();

// Output: "Global" (NOT "Local"!)

// ═══════════════════════════════════════════════════════════════
// Why?
// ═══════════════════════════════════════════════════════════════
//
// printName is DEFINED in Global scope
// So its parent lexical environment is GLOBAL
// 
// When printName looks for 'name':
// 1. Check own scope β†’ not found
// 2. Check parent (GLOBAL, where it was WRITTEN) β†’ found "Global"
//
// It doesn't matter that printName was CALLED from wrapper
// Lexical scope is about WHERE CODE IS WRITTEN!

⚠️ Scope Chain Does NOT Work Downward

function parent() {
    
    function child() {
        var childVar = "I'm in child";
    }
    
    child();
    console.log(childVar);  // ReferenceError: childVar is not defined
}

parent();

// ═══════════════════════════════════════════════════════════════
// Why the error?
// ═══════════════════════════════════════════════════════════════
//
// The REAL reason: child()'s execution context is GONE from memory!
//
// Timeline:
// 1. parent() starts executing
// 2. child() is called β†’ new execution context pushed to stack
// 3. childVar is created in child's context
// 4. child() finishes β†’ context POPPED from stack and GARBAGE COLLECTED
// 5. parent() continues β†’ tries to access childVar
// 6. But childVar doesn't exist anymore! Memory was freed!
//
// So the explanation is simple:
// - Child's execution context is destroyed when it finishes
// - Its variables are no longer in memory
// - Parent can't access what doesn't exist
//
// (Remember: only ONE execution context runs at a time)
// (Parent was PAUSED while child ran, by the time parent resumes, child is gone)

Variable Shadowing

var name = "Global Name";

function outer() {
    var name = "Outer Name";  // Shadows global name
    
    function inner() {
        var name = "Inner Name";  // Shadows outer name
        console.log(name);  // "Inner Name"
    }
    
    inner();
    console.log(name);  // "Outer Name"
}

outer();
console.log(name);  // "Global Name"

// Each scope has its own 'name' - they don't interfere with each other
// Inner name "shadows" (hides) the outer ones

Block Scope with let/const

// let/const create new scope for each block {}

if (true) {
    let blockVar = "I exist only in this block";
    const BLOCK_CONST = "Me too";
    
    console.log(blockVar);     // Works
    console.log(BLOCK_CONST);  // Works
}

console.log(blockVar);     // ReferenceError!
console.log(BLOCK_CONST);  // ReferenceError!

// var does NOT respect block scope
if (true) {
    var notBlocked = "I escape the block!";
}
console.log(notBlocked);  // "I escape the block!"
Scope Chain Rules Summary:
  • Lexical Scope - Determined by WHERE code is written, not where called
  • Upward Only - Can look UP to parents, never DOWN into children
  • Shadowing - Inner variable with same name hides outer one
  • Block Scope - let/const create new scope per block, var doesn't

πŸ”’ Closures

A closure is when a function remembers and accesses variables from its lexical environment even after the outer function has finished executing.

The Puzzle That Reveals Closures

function outer() {
    var secret = "hidden treasure";
    
    return function inner() {
        console.log(secret);
    };
}

var myFunc = outer();  // outer() finishes executing, should be gone!
myFunc();              // "hidden treasure" ← HOW?!

// ═══════════════════════════════════════════════════════════════
// The mystery:
// - outer() has finished executing
// - Its execution context was POPPED from the call stack
// - So 'secret' should be garbage collected, right?
// - But inner() can still access it!
// ═══════════════════════════════════════════════════════════════

How Closures Work

When inner() is CREATED inside outer(), it gets: inner = { code: function() { console.log(secret); }, [[Environment]]: REFERENCE to outer's lexical environment } This [[Environment]] reference keeps outer's variables ALIVE! Garbage Collector sees: "inner still has a reference to outer's environment" "Someone might need those variables!" "I WON'T free that memory!"

Closure = Function + Its Lexical Environment

// Every time you call createCounter(), you get a NEW closure
// Each closure has its own copy of 'count'

function createCounter() {
    var count = 0;  // This will be "remembered"
    
    return function() {
        count++;
        return count;
    };
}

var counter1 = createCounter();  // New closure with { count: 0 }
var counter2 = createCounter();  // Another closure with { count: 0 }

console.log(counter1());  // 1
console.log(counter1());  // 2
console.log(counter1());  // 3

console.log(counter2());  // 1 ← Different closure, different count!
console.log(counter2());  // 2

// counter1 and counter2 are completely independent
// They each "close over" their own 'count' variable

Closure Use Cases

1. Data Privacy / Encapsulation

function createBankAccount(initial) {
    // 'balance' is PRIVATE
    var balance = initial;
    
    return {
        deposit: function(amt) {
            balance += amt;
        },
        withdraw: function(amt) {
            if (amt <= balance) {
                balance -= amt;
            }
        },
        getBalance: function() {
            return balance;
        }
    };
}

var account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance());  // 1500
console.log(account.balance);  // undefined!

2. Function Factory

function createMultiplier(factor) {
    // 'factor' is remembered
    return function(number) {
        return number * factor;
    };
}

var double = createMultiplier(2);
var triple = createMultiplier(3);
var quadruple = createMultiplier(4);

console.log(double(5));     // 10
console.log(triple(5));     // 15
console.log(quadruple(5));  // 20

3. Event Handlers with State

function createButtonHandler(buttonName) {
    var clickCount = 0;
    
    return function() {
        clickCount++;
        console.log(buttonName + " clicked " + clickCount + " times");
    };
}

var saveHandler = createButtonHandler('Save');
var deleteHandler = createButtonHandler('Delete');

saveHandler();    // "Save clicked 1 times"
saveHandler();    // "Save clicked 2 times"
deleteHandler();  // "Delete clicked 1 times" ← Separate counter!
saveHandler();    // "Save clicked 3 times"

The Loop Gotcha (Closure Edition)

// ═══════════════════════════════════════════════════════════════
// Common Mistake: All closures share the same variable!
// ═══════════════════════════════════════════════════════════════

function createFunctions() {
    var functions = [];
    
    for (var i = 0; i < 3; i++) {
        functions.push(function() {
            console.log(i);
        });
    }
    
    return functions;
}

var funcs = createFunctions();
funcs[0]();  // 3 (not 0!)
funcs[1]();  // 3 (not 1!)
funcs[2]();  // 3 (not 2!)

// WHY?
// All three functions close over the SAME 'i' variable
// By the time they run, the loop is done and i = 3

// ═══════════════════════════════════════════════════════════════
// Solution 1: Use let (creates new binding each iteration)
// ═══════════════════════════════════════════════════════════════

function createFunctions() {
    var functions = [];
    
    for (let i = 0; i < 3; i++) {  // let instead of var
        functions.push(function() {
            console.log(i);
        });
    }
    
    return functions;
}

var funcs = createFunctions();
funcs[0]();  // 0 βœ“
funcs[1]();  // 1 βœ“
funcs[2]();  // 2 βœ“

Memory Perspective on Closures

// ═══════════════════════════════════════════════════════════════
// Without closure: Memory is freed when function ends
// ═══════════════════════════════════════════════════════════════

function noClosureExample() {
    var data = "some data";
    console.log(data);
    // Function ends β†’ data is garbage collected
}

noClosureExample();
// 'data' no longer exists in memory

// ═══════════════════════════════════════════════════════════════
// With closure: Memory stays alive as long as closure exists
// ═══════════════════════════════════════════════════════════════

function closureExample() {
    var data = "some data";
    
    return function() {
        console.log(data);
    };
}

var closureFunc = closureExample();
// 'data' is STILL in memory! closureFunc holds a reference to it

closureFunc();  // "some data"

// To free the memory:
closureFunc = null;  // Now 'data' can be garbage collected

πŸ‘† The this Keyword

this is a special keyword that's determined by HOW a function is called, not where it's written.

Critical Difference:
  • Lexical Scope (closures): WHERE function is WRITTEN
  • this Binding: HOW function is CALLED

The 4 Rules of this Binding (Priority Order)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RULE PRIORITY (highest to lowest) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 1. new Binding β†’ new func() β†’ new object β”‚ β”‚ 2. Explicit Binding β†’ call/apply/bind β†’ specified object β”‚ β”‚ 3. Implicit Binding β†’ obj.func() β†’ obj (before dot) β”‚ β”‚ 4. Default Binding β†’ func() β†’ window/undefined β”‚ β”‚ β”‚ β”‚ Check in this order: new > explicit > implicit > default β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1

Default Binding

func() - Plain function call β†’ this = window/undefined

function showThis() {
    console.log(this);
}

showThis();  // window (non-strict) or undefined (strict mode)

"use strict";
function strictShowThis() {
    console.log(this);
}
strictShowThis();  // undefined
2

Implicit Binding

obj.func() - Method call β†’ this = object before the dot

var person = {
    name: "Sandeep",
    age: 25,
    greet: function() {
        console.log("Hello, I'm " + this.name);
    },
    getInfo: function() {
        console.log(this.name + " is " + this.age + " years old");
    }
};

person.greet();    // "Hello, I'm Sandeep"   - this = person
person.getInfo();  // "Sandeep is 25 years old" - this = person

// ⚠️ GOTCHA: Losing implicit binding
var greetFunc = person.greet;
greetFunc();  // "Hello, undefined" ← Lost binding!

// WHY?
// greetFunc() is a plain function call (no dot!)
// So it falls back to DEFAULT binding
3

Explicit Binding (call, apply, bind)

call/apply/bind β†’ this = specified object

function introduce(greeting, punctuation) {
    console.log(greeting + ", I'm " + this.name + punctuation);
}

var person1 = { name: "Sandeep" };
var person2 = { name: "Kumar" };

// call - Immediately invokes, args passed individually
introduce.call(person1, "Hello", "!");   // "Hello, I'm Sandeep!"
introduce.call(person2, "Hi", ".");      // "Hi, I'm Kumar."

// apply - Immediately invokes, args passed as array
introduce.apply(person1, ["Hey", "!!"]);  // "Hey, I'm Sandeep!!"

// bind - Returns NEW function with this permanently bound
var introduceSandeep = introduce.bind(person1);
introduceSandeep("Namaste", "πŸ™");  // "Namaste, I'm SandeepπŸ™"

// Even if you try to change this later, bind wins:
introduceSandeep.call(person2, "Test", "!");
// "Test, I'm Sandeep!" ← Still Sandeep, not Kumar!

⚠️ bind() Stores a REFERENCE, Not a Copy

var person = { name: "Sandeep", age: 25 };

function showInfo() {
    console.log(this.name + " is " + this.age + " years old");
}

var boundFunc = showInfo.bind(person);
boundFunc();  // "Sandeep is 25 years old"

// Now mutate the object
person.name = "Kumar";
person.age = 30;

boundFunc();  // "Kumar is 30 years old" ← Sees the mutation!

// WHY?
// bind() stores a REFERENCE to the object, not a copy
// When you mutate the object, boundFunc sees the changes
// Because it still points to the same object in memory

bind() Memory Considerations

bind() creates NEW function objects β†’ can cause memory issues:
  • Each bind() creates a new function object in memory
  • 1000 components with bound handlers = 1000 extra functions
  • Event listeners need stored references for cleanup
  • call/apply have no memory overhead (invoke immediately)
// ═══════════════════════════════════════════════════════════════
// Potential Memory Leak with Event Listeners
// ═══════════════════════════════════════════════════════════════

class BadExample {
    constructor(element) {
        this.element = element;
        // Creates NEW function each time - can't remove later!
        element.addEventListener('click', this.handleClick.bind(this));
    }
    
    destroy() {
        // CAN'T remove the listener! We don't have reference to bound function!
        // element.removeEventListener('click', ???);
    }
}

// ═══════════════════════════════════════════════════════════════
// Better Pattern: Store the bound function reference
// ═══════════════════════════════════════════════════════════════

class GoodExample {
    constructor(element) {
        this.element = element;
        this.boundHandler = this.handleClick.bind(this);  // Store reference
        element.addEventListener('click', this.boundHandler);
    }
    
    destroy() {
        this.element.removeEventListener('click', this.boundHandler);
        this.boundHandler = null;  // Allow garbage collection
    }
}
4

new Binding

new func() - Constructor call β†’ this = newly created object

function Person(name, age) {
    // 1. A new empty object {} is created
    // 2. this = that new object
    // 3. The object's __proto__ is set to Person.prototype
    
    this.name = name;
    this.age = age;
    
    // 4. If no explicit return, 'this' is returned
}

var sandeep = new Person("Sandeep", 25);
console.log(sandeep.name);  // "Sandeep"
console.log(sandeep.age);   // 25

Arrow Functions: NO Own this Binding

Arrow functions don't have their own this. They inherit this from their lexical scope (where they're written).

// ═══════════════════════════════════════════════════════════════
// The Problem: Regular functions in callbacks lose 'this'
// ═══════════════════════════════════════════════════════════════

var person = {
    name: "Sandeep",
    hobbies: ["coding", "reading", "gaming"],
    
    showHobbies: function() {
        this.hobbies.forEach(function(hobby) {
            // 'this' is NOT person here! It's window/undefined
            console.log(this.name + " likes " + hobby);
        });
    }
};

person.showHobbies();
// "undefined likes coding"
// "undefined likes reading"
// "undefined likes gaming"

// ═══════════════════════════════════════════════════════════════
// Solution: Arrow function (inherits this from showHobbies)
// ═══════════════════════════════════════════════════════════════

var person = {
    name: "Sandeep",
    hobbies: ["coding", "reading", "gaming"],
    
    showHobbies: function() {
        // Arrow function has NO own this - uses parent's this
        this.hobbies.forEach((hobby) => {
            console.log(this.name + " likes " + hobby);
        });
    }
};

person.showHobbies();
// "Sandeep likes coding"
// "Sandeep likes reading"
// "Sandeep likes gaming"
// Arrow Functions Cannot be Rebound

var arrowFunc = () => {
    console.log(this);
};

var obj = { name: "Test" };

arrowFunc.call(obj);   // Still logs global/undefined
arrowFunc.apply(obj);  // Still logs global/undefined
arrowFunc.bind(obj)(); // Still logs global/undefined

// Arrow functions IGNORE call/apply/bind for 'this'
// They ALWAYS use lexical this

this Summary Table

BindingHow to Identifythis Value
Defaultfunc()window / undefined (strict)
Implicitobj.func()obj (left of dot)
Explicitcall/apply/bindspecified object
newnew Func()newly created object
Arrow() => {}inherited from lexical scope

🧬 Prototypes & Inheritance

JavaScript uses prototypal inheritance - objects inherit directly from other objects, not from classes.

Prototype Chain Lookup

When you access: obj.property 1. Look in obj itself ↓ Not found? 2. Look in obj.__proto__ (its prototype) ↓ Not found? 3. Look in obj.__proto__.__proto__ ↓ Keep going up the chain... 4. Reach null (end of chain) ↓ Not found? 5. Return undefined

Building a Prototype Chain

var animal = {
    eats: true,
    walk: function() {
        console.log("Walking...");
    }
};

var dog = Object.create(animal);  // dog's prototype = animal
dog.barks = true;
dog.bark = function() {
    console.log("Woof!");
};

var myDog = Object.create(dog);   // myDog's prototype = dog
myDog.name = "Buddy";

// ═══════════════════════════════════════════════════════════════
// Property lookups:
// ═══════════════════════════════════════════════════════════════

console.log(myDog.name);   // "Buddy"      ← Found in myDog itself
console.log(myDog.barks);  // true         ← Found in dog (prototype)
console.log(myDog.eats);   // true         ← Found in animal
myDog.walk();              // "Walking..." ← Found in animal

// Chain visualization:
//
//  myDog = { name: "Buddy" }
//    β”‚
//    └──▢ myDog.__proto__ = dog = { barks: true, bark: fn }
//           β”‚
//           └──▢ dog.__proto__ = animal = { eats: true, walk: fn }
//                  β”‚
//                  └──▢ animal.__proto__ = Object.prototype
//                         β”‚
//                         └──▢ Object.prototype.__proto__ = null

__proto__ vs .prototype

PropertyExists OnPurpose
__proto__Every objectPoints to object's prototype (parent)
.prototypeFunctions onlyBecomes __proto__ of objects created with new
function Person(name) {
    this.name = name;
}

// Person is a function, so it has .prototype
console.log(Person.prototype);  // { constructor: Person }

// Add a method to Person.prototype
Person.prototype.greet = function() {
    console.log("Hello, " + this.name);
};

var sandeep = new Person("Sandeep");

// sandeep's __proto__ points to Person.prototype
console.log(sandeep.__proto__ === Person.prototype);  // true!

// sandeep can use greet() through the prototype chain
sandeep.greet();  // "Hello, Sandeep"

Why Methods Go on Prototype (Memory Efficiency)

// Methods on prototype are SHARED (memory efficient)
function Person(name) {
    this.name = name;
}
Person.prototype.greet = function() { /* ... */ };

var p1 = new Person("Sandeep");
var p2 = new Person("Kumar");

console.log(p1.greet === p2.greet);  // true! Same function

// 1000 people = 1 shared greet function = memory efficient!
// If methods were in constructor:
// 1000 people = 1000 identical functions = wasted memory!

Inheritance with Constructor Functions

// ═══════════════════════════════════════════════════════════════
// Parent constructor
// ═══════════════════════════════════════════════════════════════

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(this.name + " makes a sound");
};

// ═══════════════════════════════════════════════════════════════
// Child constructor
// ═══════════════════════════════════════════════════════════════

function Dog(name, breed) {
    Animal.call(this, name);  // Call parent constructor with Dog's 'this'
    this.breed = breed;
}

// Set up the prototype chain
Dog.prototype = Object.create(Animal.prototype);

// Fix the constructor reference (it now points to Animal, which is wrong)
Dog.prototype.constructor = Dog;

// Add Dog-specific methods
Dog.prototype.bark = function() {
    console.log(this.name + " says: Woof!");
};

// ═══════════════════════════════════════════════════════════════
// Usage
// ═══════════════════════════════════════════════════════════════

var buddy = new Dog("Buddy", "Golden Retriever");

buddy.bark();   // "Buddy says: Woof!" (own method)
buddy.speak();  // "Buddy makes a sound" (inherited from Animal)

console.log(buddy instanceof Dog);     // true
console.log(buddy instanceof Animal);  // true

Why Dog.prototype.constructor = Dog?

function Animal(name) { this.name = name; }
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);

// At this point:
console.log(Dog.prototype.constructor);  // Animal (WRONG!)

// Object.create(Animal.prototype) creates new object
// This new object doesn't have 'constructor' property
// So it looks up the chain and finds Animal.prototype.constructor = Animal

// The fix:
Dog.prototype.constructor = Dog;  // Now correct!

// Why does it matter?
// 1. Debugging - correct constructor shows in console
// 2. Creating new instances from existing ones:
//    var anotherDog = buddy.constructor("Max", "Lab");  // Works correctly now

⚠️ Why Object.setPrototypeOf is Inefficient

var obj = { a: 1 };
var newProto = { b: 2 };

// DON'T DO THIS (slow!)
Object.setPrototypeOf(obj, newProto);

// ═══════════════════════════════════════════════════════════════
// Why is it slow?
// ═══════════════════════════════════════════════════════════════
//
// Modern JavaScript engines (V8, SpiderMonkey) create optimized
// "hidden classes" or "shapes" for objects based on their structure.
//
// When you change an object's prototype AFTER creation:
// 1. Engine has to abandon its optimizations
// 2. Must recalculate property access paths
// 3. Invalidates inline caching
// 4. All code that touches this object becomes slower
//
// It's a "de-optimization" that affects not just this operation,
// but ALL future property accesses on this object!

Why Dog.prototype.constructor = Dog is NOT Slow

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // This is FINE!

// ═══════════════════════════════════════════════════════════════
// This is just a regular property assignment!
// ═══════════════════════════════════════════════════════════════
//
// Dog.prototype.constructor = Dog is the same as:
// Dog.prototype.someProperty = someValue
//
// It's NOT changing the prototype chain structure
// It's just adding a property to an existing object
// No chain restructuring, no de-optimization
//
// All just property assignments - completely normal and fast!
Prototypes Best Practices:
  • Methods on prototype - Memory efficient (shared)
  • Data properties in constructor - Each instance gets own copy
  • Don't modify built-in prototypes - Can break other code
  • Set prototype at creation time - Changing later is slow
  • Use Object.create() - Clean prototype setup

πŸ“š ES6 Classes

ES6 Classes are syntactic sugar over the prototype-based inheritance. Under the hood, they work exactly the same way!

Key Insight: ES6 Classes don't introduce a NEW inheritance model. They're just a cleaner way to write constructor functions, prototype methods, and inheritance chains. Same prototype system underneath!

Constructor Function vs Class

// ═══════════════════════════════════════════════════════════════
// OLD WAY: Constructor Function + Prototype
// ═══════════════════════════════════════════════════════════════

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    console.log("Hello, I'm " + this.name);
};

Person.prototype.getAge = function() {
    return this.age;
};

var person1 = new Person("Sandeep", 25);


// ═══════════════════════════════════════════════════════════════
// NEW WAY: ES6 Class (same thing, cleaner syntax!)
// ═══════════════════════════════════════════════════════════════

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        console.log("Hello, I'm " + this.name);
    }
    
    getAge() {
        return this.age;
    }
}

const person1 = new Person("Sandeep", 25);


// ═══════════════════════════════════════════════════════════════
// BOTH produce the EXACT same result in memory!
// ═══════════════════════════════════════════════════════════════

Class Syntax Breakdown

class Person {
    // ─────────────────────────────────────────────────────────────
    // CONSTRUCTOR: Called when you do `new Person()`
    // ─────────────────────────────────────────────────────────────
    constructor(name, age) {
        this.name = name;      // Instance property
        this.age = age;        // Instance property
    }
    
    // ─────────────────────────────────────────────────────────────
    // INSTANCE METHODS: Added to Person.prototype (shared!)
    // ─────────────────────────────────────────────────────────────
    greet() {
        console.log("Hello, I'm " + this.name);
    }
    
    // ─────────────────────────────────────────────────────────────
    // GETTER: Access like property (person.info)
    // ─────────────────────────────────────────────────────────────
    get info() {
        return `${this.name}, ${this.age} years old`;
    }
    
    // ─────────────────────────────────────────────────────────────
    // SETTER: Set like property (person.age = 30)
    // ─────────────────────────────────────────────────────────────
    set age(value) {
        if (value < 0) throw new Error("Age can't be negative");
        this._age = value;
    }
    
    get age() {
        return this._age;
    }
    
    // ─────────────────────────────────────────────────────────────
    // STATIC METHOD: Called on class, not instance (Person.create())
    // ─────────────────────────────────────────────────────────────
    static create(name) {
        return new Person(name, 0);
    }
}

const person = new Person("Sandeep", 25);
person.greet();              // "Hello, I'm Sandeep"
console.log(person.info);    // "Sandeep, 25 years old" (getter)

const baby = Person.create("Baby");  // Static method

Class Inheritance (extends & super)

// ═══════════════════════════════════════════════════════════════
// OLD WAY: Prototype chain manually (verbose!)
// ═══════════════════════════════════════════════════════════════

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(this.name + " makes a sound");
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log(this.name + " barks!");
};


// ═══════════════════════════════════════════════════════════════
// NEW WAY: extends & super (so much cleaner!)
// ═══════════════════════════════════════════════════════════════

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        console.log(this.name + " makes a sound");
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);  // Calls Animal's constructor (REQUIRED!)
        this.breed = breed;
    }
    
    bark() {
        console.log(this.name + " barks!");
    }
    
    // Override parent method
    speak() {
        console.log(this.name + " says: Woof!");
    }
}

const buddy = new Dog("Buddy", "Golden Retriever");
buddy.speak();  // "Buddy says: Woof!" (overridden)
buddy.bark();   // "Buddy barks!"

console.log(buddy instanceof Dog);     // true
console.log(buddy instanceof Animal);  // true

The super Keyword

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        console.log(this.name + " makes a sound");
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        // ─────────────────────────────────────────────────────────
        // super() MUST be called before using `this` in child class
        // ─────────────────────────────────────────────────────────
        super(name);  // Calls parent constructor
        this.breed = breed;
    }
    
    speak() {
        // ─────────────────────────────────────────────────────────
        // super.method() calls parent's method
        // ─────────────────────────────────────────────────────────
        super.speak();  // Call parent's speak
        console.log("...and then barks!");
    }
}

const buddy = new Dog("Buddy", "Lab");
buddy.speak();
// "Buddy makes a sound"
// "...and then barks!"

Private Fields (ES2022)

class BankAccount {
    // ─────────────────────────────────────────────────────────────
    // # makes field PRIVATE (can't access outside class)
    // ─────────────────────────────────────────────────────────────
    #balance;
    #pin;
    
    constructor(initialBalance, pin) {
        this.#balance = initialBalance;
        this.#pin = pin;
    }
    
    deposit(amount) {
        this.#balance += amount;
        console.log("Deposited:", amount);
    }
    
    withdraw(amount, pin) {
        if (pin !== this.#pin) {
            console.log("Wrong PIN!");
            return;
        }
        if (amount > this.#balance) {
            console.log("Insufficient funds!");
            return;
        }
        this.#balance -= amount;
        console.log("Withdrew:", amount);
    }
    
    getBalance(pin) {
        if (pin !== this.#pin) return "Wrong PIN!";
        return this.#balance;
    }
}

const account = new BankAccount(1000, "1234");

account.deposit(500);
console.log(account.getBalance("1234"));  // 1500

// Trying to access private fields:
console.log(account.#balance);  // SyntaxError: Private field!
console.log(account.balance);   // undefined (not the same)

Static Members

class MathUtils {
    // ─────────────────────────────────────────────────────────────
    // Static properties & methods belong to CLASS, not instances
    // ─────────────────────────────────────────────────────────────
    static PI = 3.14159;
    
    static square(x) {
        return x * x;
    }
    
    static cube(x) {
        return x * x * x;
    }
}

// Called on CLASS, not instance
console.log(MathUtils.PI);         // 3.14159
console.log(MathUtils.square(5));  // 25
console.log(MathUtils.cube(3));    // 27

// Can't call on instance
const math = new MathUtils();
console.log(math.PI);        // undefined
console.log(math.square(5)); // TypeError: not a function


// ═══════════════════════════════════════════════════════════════
// Common use case: Factory methods, counters, utilities
// ═══════════════════════════════════════════════════════════════

class User {
    static #count = 0;  // Private static
    
    constructor(name) {
        this.name = name;
        this.id = ++User.#count;  // Auto-increment ID
    }
    
    static getCount() {
        return User.#count;
    }
}

const user1 = new User("Sandeep");  // id: 1
const user2 = new User("Kumar");    // id: 2
console.log(User.getCount());       // 2

Class vs Function Differences

FeatureClassConstructor Function
Hoisting❌ Not hoisted (TDZ)βœ… Fully hoisted
Strict ModeAlways strictDepends on context
Must use newβœ… Required❌ Optional (but should)
Methods enumerable❌ Non-enumerableβœ… Enumerable by default
typeof"function""function"
// ═══════════════════════════════════════════════════════════════
// 1. Classes are NOT hoisted like function declarations
// ═══════════════════════════════════════════════════════════════

const p = new Person("Test");  // ReferenceError!
class Person { }

// Functions are hoisted:
const p = new PersonFunc("Test");  // Works!
function PersonFunc(name) { this.name = name; }


// ═══════════════════════════════════════════════════════════════
// 2. Classes ALWAYS run in strict mode
// ═══════════════════════════════════════════════════════════════

class StrictClass {
    method() {
        console.log(this);  // undefined if called without context
    }
}

const obj = new StrictClass();
const method = obj.method;
method();  // undefined (not window!)


// ═══════════════════════════════════════════════════════════════
// 3. Must use 'new' with classes
// ═══════════════════════════════════════════════════════════════

class Person {
    constructor(name) { this.name = name; }
}

Person("Sandeep");      // TypeError: Cannot call without 'new'
new Person("Sandeep");  // Works!

Proof That Classes Are Just Functions

class Person {
    constructor(name) {
        this.name = name;
    }
    greet() {
        console.log("Hello!");
    }
}

// Proof that class is just a function:
console.log(typeof Person);  // "function"

// Proof that methods are on prototype:
console.log(Person.prototype.greet);  // function greet()

// Proof that instances work the same:
const p = new Person("Sandeep");
console.log(p.__proto__ === Person.prototype);  // true

// You can even add to the prototype:
Person.prototype.wave = function() {
    console.log(this.name + " waves!");
};
p.wave();  // "Sandeep waves!"

ES6 Classes Cheat Sheet

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ES6 CLASSES CHEAT SHEET β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ class Person { β”‚ β”‚ #privateField; // Private (ES2022) β”‚ β”‚ publicField = value; // Public field β”‚ β”‚ β”‚ β”‚ constructor() { } // Called with 'new' β”‚ β”‚ β”‚ β”‚ method() { } // Instance method (on prototype) β”‚ β”‚ get prop() { } // Getter β”‚ β”‚ set prop(v) { } // Setter β”‚ β”‚ β”‚ β”‚ static method() { } // Class method (not on instance) β”‚ β”‚ static field = v; // Class property β”‚ β”‚ } β”‚ β”‚ β”‚ β”‚ class Child extends Parent { β”‚ β”‚ constructor() { β”‚ β”‚ super(); // MUST call before using 'this' β”‚ β”‚ } β”‚ β”‚ method() { β”‚ β”‚ super.method(); // Call parent method β”‚ β”‚ } β”‚ β”‚ } β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ KEY DIFFERENCES FROM FUNCTIONS: β”‚ β”‚ β€’ Not hoisted (TDZ applies) β”‚ β”‚ β€’ Always strict mode β”‚ β”‚ β€’ Must use 'new' β”‚ β”‚ β€’ Methods are non-enumerable β”‚ β”‚ β”‚ β”‚ UNDER THE HOOD: β”‚ β”‚ β€’ Still prototype-based β”‚ β”‚ β€’ typeof class === "function" β”‚ β”‚ β€’ Methods still on .prototype β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ” The Event Loop

JavaScript is single-threaded β€” one call stack, one thing at a time. Yet it handles timers, network requests, and user clicks without blocking. The mechanism that makes this possible is the Event Loop: a coordinator that decides what runs next when the call stack is empty.

The one-sentence model: Run all synchronous code to completion β†’ then drain every microtask β†’ (maybe render) β†’ run one macrotask β†’ repeat forever. Get this loop right and every "what's the output order?" question becomes mechanical.

The Players

Five pieces work together. The Event Loop only does one job: when the Call Stack is empty, it moves the next callback into it.

🧱 Call Stack SYNC

LIFO. Runs your code frame by frame. The event loop only acts when this is empty.

printSquare(4)
square(4)
multiply(4, 4)
🌐 Web / Node APIs ASYNC

Provided by the browser/Node, NOT the JS engine. Timers, fetch, DOM events run here off-thread.

setTimeout timer ⏱️
fetch() 🌍
addEventListener πŸ‘†
⚑ Microtask Queue HIGH PRIORITY

Promises, await continuations, queueMicrotask, MutationObserver. Fully drained after each task.

.then callback
await continuation
πŸ“₯ Macrotask Queue ONE PER TICK

setTimeout/setInterval, I/O, message events, UI events. Only one runs per loop iteration.

setTimeout callback
click handler
πŸ”„ The Event Loop

"Is the call stack empty? β†’ drain ALL microtasks β†’ render if needed β†’ take ONE macrotask β†’ repeat."

Synchronous (call stack) Web API (off-thread) Microtask (Promise/await) Macrotask (setTimeout/events) Output

The Algorithm (one "tick" of the loop)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ONE EVENT LOOP ITERATION β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ 1. Run the current task (script / a macrotask) to completion β”‚ (call stack grows & shrinks β€” runs to EMPTY) β–Ό 2. Drain the MICROTASK queue ──► run them ALL, in order, β”‚ including any NEW microtasks β”‚ they schedule (until empty!) β–Ό 3. (If it's time) RENDER ──► requestAnimationFrame β†’ style β†’ layout β†’ paint β”‚ β–Ό 4. Take ONE macrotask from the queue ──► back to step 1 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ KEY: microtasks ALL run before the next macrotask. β”‚ β”‚ That's the rule behind 90% of ordering questions. β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Scenario 1 Β· The Call Stack (synchronous, LIFO)

Before any async, understand the stack. Functions push on when called and pop off when they return β€” Last In, First Out.

function multiply(a, b) { return a * b; }
function square(n)    { return multiply(n, n); }
function printSquare(n) {
  const s = square(n);
  console.log(s);
}
printSquare(4); // β†’ 16
push β†’ printSquare(4) printSquare(4) printSquare(4) square(4) square(4) multiply(4,4) β—„ returns 16 ───────────── ───────────── ───────────── pop ← (everything unwinds back down, then console.log(16))
If a synchronous function takes 5 seconds, the whole page freezes β€” nothing else can run because the single stack is busy. That's why we push slow work to Web APIs.

Scenario 2 Β· setTimeout β€” a Macrotask

console.log('1. Start');
setTimeout(() => console.log('3. Timeout (even with 0ms!)'), 0);
console.log('2. End');

// Output:  1. Start  β†’  2. End  β†’  3. Timeout

Even at 0ms, the callback does not run immediately. setTimeout hands the timer to a Web API; when it fires, the callback joins the macrotask queue and only runs once the stack is empty and all microtasks are done.

log "Start" β†’ timer (Web API) β†’ log "End" β†’ log "Timeout"
"0ms" is a minimum, not a promise. The HTML spec also clamps nested timeouts to β‰₯4ms. setTimeout(fn, 0) really means "run this as soon as the stack and microtasks are clear."

Scenario 3 Β· Promise (Microtask) vs setTimeout (Macrotask)

This is the heart of it. Microtasks always beat macrotasks, no matter the order they were queued.

console.log('1. sync');
setTimeout(() => console.log('4. macrotask'), 0);
Promise.resolve().then(() => console.log('3. microtask'));
console.log('2. sync');

// Output:  1. sync  β†’  2. sync  β†’  3. microtask  β†’  4. macrotask

⚑ Microtask Queue

Drained completely after the current task, before any rendering or macrotask.

Promise .then/.catch/.finally, await, queueMicrotask, MutationObserver

πŸ“₯ Macrotask Queue

One is taken per loop iteration β€” then microtasks drain again.

setTimeout, setInterval, setImmediate (Node), I/O, MessageChannel, UI events

Scenario 4 Β· async / await (it's Promises in disguise)

An async function runs synchronously up to the first await. Everything after the await becomes a microtask continuation.

async function foo() {
  console.log('2. inside foo (sync until await)');
  await null;                       // ⏸ pause β€” schedule rest as a microtask
  console.log('4. after await (microtask)');
}

console.log('1. start');
foo();
console.log('3. end');

// Output:  1. start β†’ 2. inside foo β†’ 3. end β†’ 4. after await
Mental swap: await x β‰ˆ Promise.resolve(x).then(() => /* the rest of the function */). So the line after every await is a microtask β€” it waits for the sync code to finish first.

Scenario 5 Β· Microtask Starvation (how to freeze a tab)

Because microtasks drain completely β€” including new ones they schedule β€” an infinite chain of microtasks never lets the loop reach rendering or macrotasks. The page hangs.

❌ Freezes the page

function loop() {
  Promise.resolve().then(loop);
}
loop(); // microtask queue
// never empties β†’ no paint,
// no clicks, no timers. DEAD.

βœ… Stays responsive

function loop() {
  setTimeout(loop, 0);
}
loop(); // macrotask yields
// between iterations β†’ browser
// can paint & handle input.
Rule of thumb: for chunked heavy work that must not block the UI, yield with macrotasks (setTimeout), requestIdleCallback, or scheduler.postTask β€” never recursive microtasks.

Scenario 6 Β· The Classic Interview Puzzle (full trace)

Put it all together. Predict the output, then follow the trace.

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
  console.log('3');
  setTimeout(() => console.log('4'), 0);
});
(async () => {
  console.log('5');
  await Promise.resolve();
  console.log('6');
})();
setTimeout(() => console.log('7'), 0);
console.log('8');
Sync pass (run the script to the end): logs 1, then 5 (the async IIFE runs synchronously until its await), then 8. Three timers and two microtasks are now queued.
Output: 1, 5, 8
Drain ALL microtasks: the .then runs β†’ logs 3 (and queues a 3rd timer for 4); the await continuation runs β†’ logs 6.
Output: 1, 5, 8, 3, 6
Now macrotasks, one at a time (microtasks drain after each β€” none left here): timer 2, then timer 7, then timer 4 (it was queued later, inside the microtask).
Output: 1, 5, 8, 3, 6, 2, 7, 4
1 5 8 β€– 3 6 β€– 2 7 4

Blue/green = sync Β· purple = microtasks (drain first) Β· red = macrotasks (one per tick, in queue order).

Scenario 7 Β· Where Rendering & requestAnimationFrame Fit

The browser tries to paint ~60Γ—/sec. Rendering happens after microtasks, between macrotasks. requestAnimationFrame callbacks run as part of that render step β€” right before paint.

... run a task ... β”‚ β–Ό drain microtasks (Promises / await) β”‚ β–Ό requestAnimationFrame callbacks ◄── mutate DOM here for smooth animation β”‚ β–Ό style β†’ layout β†’ paint (the actual pixels) β”‚ β–Ό next macrotask (setTimeout, events) ...
// Smooth: change layout-driving styles inside rAF, just before paint
requestAnimationFrame(() => {
  box.style.transform = 'translateX(200px)';
});

// For work that must run AFTER paint (e.g. measure), double-rAF or a timeout:
requestAnimationFrame(() => requestAnimationFrame(measureAfterPaint));

Macrotask vs Microtask β€” Source Cheatsheet

Schedules a MICROTASK ⚑Schedules a MACROTASK πŸ“₯
Promise.then / catch / finallysetTimeout / setInterval
await (continuation)setImmediate (Node)
queueMicrotask()requestAnimationFrame* (render step)
MutationObserverDOM events (click, scroll, input)
process.nextTick (Node, even higher priority)MessageChannel / postMessage, I/O

*requestAnimationFrame isn't strictly a macrotask β€” it runs in the dedicated render phase before paint β€” but it behaves "task-like": it yields, unlike a microtask.

The Rules That Answer Everything

1
Synchronous code runs first, to completion. The event loop never interrupts a running task β€” it only acts when the call stack is empty.
2
Microtasks drain entirely before the next macrotask β€” including microtasks scheduled by other microtasks. It's a full drain, not one-at-a-time.
3
Exactly one macrotask runs per loop iteration, then microtasks drain again, then maybe a render. Macrotasks run in the order they were queued.
4
async functions are synchronous until the first await. Everything after an await is a microtask.
Common gotchas:
  • setTimeout(fn, 0) is not immediate β€” sync code and all microtasks go first.
  • Recursive Promises/queueMicrotask can starve rendering and freeze the tab.
  • A long synchronous loop blocks everything β€” break heavy work into macrotasks.
  • In Node, process.nextTick outranks the Promise microtask queue; the loop also has phases (timers, poll, check) β€” similar idea, more stages.
🎀 Interview Insight

Event loop: the microtask drain rule & starvation

Surfaced in a senior mock interview. See the full Q&A β†’

Predict the output

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => { console.log('3'); setTimeout(() => console.log('4'), 0); });
(async () => { console.log('5'); await Promise.resolve(); console.log('6'); })();
setTimeout(() => console.log('7'), 0);
console.log('8');
// β†’ 1, 5, 8, 3, 6, 2, 7, 4

Sync pass logs 1, 5, 8 (an async function runs synchronously until the first await). Then microtasks 3, 6, then macrotasks 2, 7, and finally 4 (scheduled inside a microtask).

The rule

After the initial script and after every macrotask, the engine drains the entire microtask queue to zero before the next macrotask β€” and render/paint happens at that same checkpoint. It's a full drain, not a one-at-a-time tiebreak.

Microtask starvation

Recursively scheduling microtasks (.then β†’ .then β†’ …) means the queue never empties, so macrotasks, input events, and rendering never run β€” the tab hangs. For chunked heavy work, yield via macrotasks (setTimeout 0) / requestIdleCallback / scheduler.postTask, not microtasks.

JavaScript Fundamentals Guide | Last Updated: January 2026